אוטומציית PC kitting עם winget + PowerShell — הפיכת ספר הריצה לניתן-להרצה
· עודכן בתאריך: · Go Komura · winget, PowerShell, Windows, PC kitting, מערכות מידע, אוטומציה, שיפור תפעולי, יעילות עסקית
בכל פעם שנכנס עובד חדש, או שמחליפים מחשב, מישהו עובר על ספר הריצה ומגדיר מכונות אחת-אחת. במחלקת ה-IT של חברה קטנה או בינונית, מה שמכביד הוא לא רק מספר השעות שזה דורש. עבודה ידנית יוצרת שונות בהגדרות, ובין ספרי ריצה מיושנים לתחלופת עובדים נשארות בעיות מהצורה “רק המכונה הזאת מוגדרת אחרת”.
כדי לצמצם את זה, מחליפים את ספר הריצה בקובצי הגדרה ובסקריפטים שאפשר להריץ שוב. את התקנת האפליקציות משאירים ל-winget, מנהל החבילות של Windows, את החלקים שרוצים לנהל באופן הצהרתי כותבים ב-WinGet Configuration, ואת ההגדרות שאף אחד מהשניים לא מכסה משלימים ב-PowerShell.
הדבר הראשון שמפרידים בתכנון הוא “מה מתקינים” מול “באילו הרשאות מגדירים”. גם כשהאפליקציות מותקנות, ה-kitting לא נגמר אם הכוננים והמדפסות לא נראים למשתמש. המאמר הזה עובר על היסודות של התקנת אפליקציות, אחר כך על מימוש שמתפצל לשלב מנהל ולשלב משתמש, ובסוף על הבדיקות להרצה לא-מאוישת.
להתחיל מהבעיה שיש לכם
| מה רוצים לבדוק | מה להבין קודם | איפה לקרוא |
|---|---|---|
| ההתקנה נתקעת בהמתנה לאישור | להפריד בין מזהה היעד, הרצה לא-מאוישת והסכמה לתנאי השימוש | פרק 2: התקנה לא-מאוישת |
| רוצים להתקין לכל המשתמשים | האם קיים installer שתומך ב-machine scope | פרק 2: בדיקת ה-scope |
| רוצים להעביר את התצורה של מחשב תקני למחשב חדש | שחזור רשימת החבילות והעברת ההגדרות הם שני דברים נפרדים | פרק 3: export / import |
| לא בטוחים אם להשתמש ב-YAML או בסקריפט משלכם | מה כל אחד מכסה, ואיך נמנעים מניהול כפול של רשימת האפליקציות | פרק 4: Configuration, סעיף 4.2: חלוקת העבודה |
| רוצים לדעת איזה חלק בדוגמה הארוכה לשנות | קובץ ה-JSON של ההגדרה, ושני סקריפטים שרצים ברמות הרשאה שונות | פרק 5: התמונה הכוללת, מפתחות ההגדרה |
| הכוננים והמדפסות שמיפיתם לא נראים | להגדיר אותם ללא הרשאות מוגברות, בכניסה של המשתמש | שלב המשתמש, איך להפעיל |
| רוצים לתכנן הרצה חוזרת אחרי כשל באמצע | המצב הנוכחי, הבדיקה שאחרי ההתקנה, הלוגים, קודי היציאה | שלב המנהל, קודי יציאה, בדיקות בתכנון |
| רוצים להריץ מ-SYSTEM או מכלי הפצה | אימות עם חשבון ההרצה שבאמת ישמש | פרק 6: מודול PowerShell, פרק 7: הרצה לא-מאוישת |
| רוצים לעבור על הקבצים להפצה ועל ההחלטות הסופיות | התנאים שבהם הדוגמה תקפה, והערכים לארגון שלכם | פרק 8: טבלת החלטות, קוד לדוגמה |
לקריאה רצופה התחילו מפרק 1. אם אתם רוצים ישר למימוש, התחילו ממבנה שלושת החלקים בפרק 5.
לפני שאתם מריצים את הדוגמאות: נתיבים, שמות שרתים וכדומה הם דוגמה בלבד. אל תריצו אותם כמו שהם בסביבת production. התאימו אותם לסביבה שלכם ואמתו את ההתנהגות על מכונת בדיקה. היקף האימות של דוגמאות ההפצה מתואר בקטע ההורדה.
המאמר הזה מארגן מחדש תוכן שפורסם לראשונה ביולי 2026 ועודכן ב-2 באוגוסט. בדקו את דרישות הכלים ואת המגבלות ב-system context מול הגרסה שאתם פורסים ומול חשבון ההרצה שלכם. תאריך העדכון של המאמר ואימות בסביבת היעד הם שני דברים שונים.
1. קודם המסקנה
להפריד בין התקנת אפליקציות לשחזור הגדרות
- את התקנת האפליקציות משאירים ל-winget.
winget installתומך בהתקנה שקטה כברירת מחדל.1 - אפשר לחלץ את התצורה של מחשב קיים עם
winget export. אבל זה מוגבל לחבילות שנמצאות בניהול של winget, וההגדרות לא נכללות.2לבחור איך לנהל את התצורה
- אם רוצים גישה הצהרתית, משתמשים ב-WinGet Configuration (
winget configure). היא מבוססת על PowerShell DSC ומאפשרת לכתוב אפליקציות והגדרות לקובץ YAML אחד. הדרישות הן Windows 10 1809 ומעלה + winget 1.6 ומעלה.3 - מה ש-winget לא מכסה משלימים ב-PowerShell. מדפסות, כוננים משותפים, ה-Registry, תכונות Windows, חשבונות מקומיים וכדומה.
- כדי לשלוט בזה מ-PowerShell יש את המודול
Microsoft.WinGet.Client. הוא מספק cmdlets כמוInstall-WinGetPackage.4לתכנן את חשבון ההרצה ואת ההרצות החוזרות
- הרצה ב-system context (הרצה תחת חשבון שונה מהמשתמש המחובר, כמו חשבון SYSTEM) דורשת זהירות. Microsoft עדיין רושמת אותה כפריט פיתוח עתידי, ולכן חובה לאמת עם חשבון ההרצה שבאמת ישמש.3
- תמיד להבטיח idempotency (קבלת אותה תוצאה לא משנה כמה פעמים זה רץ). kitting נכשל באמצע, וחייב להיות אפשרי להריץ אותו שוב כמה פעמים שצריך.
- אל תכריחו אפליקציות פנימיות להיכנס ל-winget; הרצה שקטה של installer מ-PowerShell היא האפשרות הריאלית.
ב-diagram, solid line מציינת relation שתמיד מתקיים ו-dashed line מציינת relation מותנה (התנאים מופיעים בהסבר של כל relation ב-detail page). הרשימה המלאה של ה-relations (סה”כ 20, כולל evidence ו-certainty) וההגדרות של ה-concepts המרכזיים נמצאות ב-detail page של ה-knowledge map (ביפנית). Data: JSON-LD / Turtle
2. יסודות winget — איך כותבים התקנה לא-מאוישת
לציין את מזהה היעד, את ההרצה הלא-מאוישת ואת ההסכמות
מתחילים מתבנית הבסיס להתקנה אמינה בלי שום אינטראקציה.1
# לציין את ה-ID בדיוק ולהתקין (-e הוא התאמה מדויקת, --id מציין את ה-ID)
# --silent : לא להציג ממשק
# --accept-package-agreements : הסכמה לתנאי השימוש של החבילה
# --accept-source-agreements : הסכמה לתנאי השימוש של ה-source
# --scope machine : התקנה לכל המשתמשים (רק בחבילות שתומכות בכך)
# את ה-backtick של המשך השורה שמים בסוף השורה. הערה שאחריו שוברת את המשך השורה
winget install --id Google.Chrome -e --silent `
--accept-package-agreements --accept-source-agreements --scope machine
# לאתר את ה-ID
winget search "Visual Studio Code"
winget show --id Microsoft.VisualStudioCode
תשכחו מאפשרויות ה---accept-* וההרצה הלא-מאוישת תיתקע בהמתנה להסכמה. זה הדבר הראשון שאנשים נתקלים בו כשהם מאוטמטים kitting.
לבדוק קודם אם יש תמיכה בהתקנה לכל המשתמשים
--scope machine לא עובד עם כל חבילה; יש אפליקציות שאפשר להתקין רק פר-משתמש. במקרה כזה מסדרים שהן ירוצו ב-user context בכניסה הראשונה.
אפשר לבדוק את התמיכה לפני ההתקנה. winget show מקבל --scope, כך שאפשר לגלות מראש אם קיים installer ל-machine scope.5
# לבדוק אם קיים installer ל-machine scope
winget show --id Google.Chrome -e --scope machine
# להסתכל גם על ה-user scope, לשם השוואה
winget show --id Google.Chrome -e --scope user
אם אין installer שמתאים ל-scope שציינתם, מתקבלת הודעה שאומרת זאת. הריצו את זה על כל החבילות ביעד לפני שאתם כותבים "scope": "machine" לקובץ ההגדרה של ה-kitting, וכך תימנעו מלהיתקל בכשל בפעם הראשונה דווקא באמצע הרצה לא-מאוישת.
3. חילוץ התצורה של המחשב הנוכחי — export / import
מתחילים בייצוא רשימת החבילות
אם כבר יש לכם “מחשב תקני” מוגדר, אפשר להפוך את התצורה שלו לקובץ.2
# לייצא את רשימת החבילות המותקנות מהמחשב התקני ל-JSON
winget export --output D:\kitting\apps.json --include-versions
# לשחזר אותה במחשב החדש
winget import --import-file D:\kitting\apps.json `
--accept-package-agreements --accept-source-agreements --ignore-unavailable
ל-JSON שיוצא יש את המבנה הזה (הערכים הם דוגמה).2
{
"CreationDate": "2026-07-25T10:40:00.000-00:00",
"Sources": [
{
"Packages": [
{ "PackageIdentifier": "Google.Chrome", "Version": "126.0.6478.127" },
{ "PackageIdentifier": "Microsoft.VisualStudioCode", "Version": "1.101.2" },
{ "PackageIdentifier": "7zip.7zip", "Version": "24.09" }
],
"SourceDetails": {
"Argument": "https://cdn.winget.microsoft.com/cache",
"Identifier": "Microsoft.Winget.Source_8wekyb3d8bbwe",
"Name": "winget",
"Type": "Microsoft.PreIndexed.Package"
}
}
],
"WinGetVersion": "1.9.25180"
}
מה נכנס ל-JSON, ומה מגדירים בנפרד
כמו שרואים, התוכן הוא רשימה של מזהי חבילות וגרסאות (בלי --include-versions אין Version, ו-import מתקין את הגרסה האחרונה).2 במילים אחרות, כל מה ש-winget import עושה הוא “להתקין את החבילה עם המזהה הזה” — לא תו אחד של הגדרות אפליקציה, וגם לא אפליקציות שהותקנו בדרך אחרת מלבד winget, נמצא כאן. פתחו את ה-JSON בעצמכם והמגבלה הופכת למוחשית.
הטבלה שלמטה מפרידה בין מה שמשחזר את רשימת החבילות לבין ההגדרות שצריך לספק בנפרד.
| מה זה כן עושה | מה זה לא עושה |
|---|---|
| לשחזר את רשימת החבילות שנמצאות בניהול של winget | להעביר הגדרות מתוך האפליקציה |
לנעול גרסאות (--include-versions) |
אפליקציות שהותקנו בדרך אחרת מלבד winget, ואפליקציות פנימיות |
לדלג על חבילות שלא קיימות (--ignore-unavailable) |
הפעלת רישיון ומצב התחברות |
אז winget import הוא נקודת התחלה, לא כל ה-kitting. איך משלימים את השאר — זה הנושא האמיתי.
4. לכתוב את זה באופן הצהרתי — WinGet Configuration
לכתוב את המצב הרצוי, לא את הפרוצדורה
winget configure הוא מנגנון שמצהיר בקובץ YAML על המצב שאליו רוצים להגיע ומחיל אותו דרך PowerShell DSC.3 יש לו שלושה יתרונות על פני סקריפט פרוצדורלי.
- הוא לא עושה דבר כשהמצב הרצוי כבר קיים (idempotent)
- התקנת אפליקציות, הגדרות של Windows והגדרות של אפליקציות יכולות לבוא לידי ביטוי בקובץ אחד
- אם הוא נכשל באמצע, פשוט מריצים שוב את אותו קובץ
הדוגמה הבאה מציגה את המבנה. קראו את סעיף 4.1 כדי להבין מה המשמעות של allowPrerelease וכדי לקרוא את האזהרה על שמירת ההגדרה עקבית בתוך אותו סוג resource, לפני שאתם כותבים קובץ לארגון שלכם.
# kitting.winget — הצהרה על המצב הרצוי של מחשב תקני
properties:
configurationVersion: 0.2.0
resources:
- resource: Microsoft.WinGet.DSC/WinGetPackage
id: chrome
directives:
description: התקנת Google Chrome
allowPrerelease: true
settings:
id: Google.Chrome
source: winget
- resource: Microsoft.WinGet.DSC/WinGetPackage
id: vscode
directives:
description: התקנת Visual Studio Code
settings:
id: Microsoft.VisualStudioCode
source: winget
- resource: Microsoft.Windows.Developer/DeveloperMode
id: devmode
directives:
description: הפעלת Developer Mode (רק במחשבי פיתוח)
allowPrerelease: true
settings:
Ensure: Present
# לעבור על התוכן לפני ההחלה (מראה מה יורץ)
winget configure show --file D:\kitting\kitting.winget
# להחיל אותו
winget configure --file D:\kitting\kitting.winget --accept-configuration-agreements
לבדוק את הדרישות של היעד
הדרישות הן Windows 10 גרסה 1809 (build 17763) ומעלה, או Windows 11, ו-winget 1.6.2631 ומעלה.3 בסביבות שבהן נשארו מכונות ישנות, ההגדרה מבוססת ה-PowerShell בפרק הבא היא האפשרות הבטוחה יותר.
4.1. איך לקרוא את directives — allowPrerelease לא מדבר על האפליקציה
האפליקציה נכנסת ל-settings, ההוראות ל-resource נכנסות ל-directives
directives הוא מה שתמיד מבלבל כשמצרפים YAML מדוגמאות אחרות. מה שנכנס לשם הוא הוראות על איך מטפלים ב-resource, ולא המפרט של האפליקציה להתקנה (זה שייך לצד ה-settings).
description— הטקסט התיאורי שמוצג בזמן הריצה. הוא הופך את הפלט שלwinget configure showלקריא כפי שהוא, ולכן תמיד כותבים אותוallowPrerelease— הוראה שמתירה שימוש בגרסת prerelease של מודול ה-PowerShell שמספק את ה-resource של DSC. זו לא אומרת “להתקין גרסת prerelease (בטא) של האפליקציה”.
לשמור על ההגדרה עקבית בתוך אותו סוג resource
ההבחנה הזאת מתחילה להיות חשובה כאן. allowPrerelease הוא עניין של מודול, ולכן כשהערך שונה בין רשומות שמשתמשות באותו resource:, מדובר בדרך כלל בחוסר עקביות שנוצר מהרכבת דוגמאות זו על זו. בדוגמה שלמעלה הוא מוגדר רק ב-chrome, אחת משתי הרשומות של Microsoft.WinGet.DSC/WinGetPackage, אבל אין סיבה שיהיה שונה בין אפליקציות, ולכן שומרים עליו עקבי בתוך אותו סוג resource. הקריטריון היחיד הוא אם פורסמה גרסה יציבה של המודול שמספק את ה-resource: אם יש גרסה יציבה — מורידים אותו, ומגדירים אותו רק כשמשתמשים במודול שעדיין קיים רק כ-prerelease.
4.2. חלוקת העבודה בין WinGet Configuration ל-PowerShell שלכם
“אם WinGet Configuration הוא idempotent, למה לכתוב קוד idempotent משלנו בפרק הבא?” — זו שאלה הוגנת. התשובה היא שמה שכל אחד מכסה שונה; הם לא מחליפים זה את זה. הנה הפירוט לפי דרישה.
| דרישה | WinGet Configuration | PowerShell משלכם (פרק 5) | מה לבחור בפועל |
|---|---|---|---|
| התקנת אפליקציות שמפורסמות ב-winget | ◎ ה-resource התקני WinGetPackage |
○ אפשר לכתוב, אבל את ה-idempotency עושים בעצמכם | Configuration |
| הגדרות Windows (Developer Mode וכדומה) | ○ אם קיים resource מתאים של DSC | ○ | Configuration אם ה-resource קיים |
| ערכי Registry שרירותיים (הגדרות תקן פנימיות) | △ תלוי ב-resource שזמין | ◎ אפשר לכתוב הכול | PowerShell |
| אפליקציות פנימיות בתיקייה משותפת (MSI/EXE) | △ מחוץ לתחום של ה-resources התקניים | ◎ | PowerShell |
| כונני רשת ומדפסות משותפות | × עבודה פר-משתמש שמתרחשת בכניסה | ◎ | PowerShell (השלב הלא-מוגבר) |
| הבדלים לפי מחלקה או דגם מחשב | △ לפצל את ה-YAML | ◎ החלפת קובץ ה-JSON של ההגדרה מספיקה | PowerShell |
| מערכת ההפעלה ביעד ישנה (לפני 1809, winget לפני 1.6) | × לא עומד בדרישות | ◎ | PowerShell |
| החזרת המידע אם נדרשת הפעלה מחדש לכלי ההפצה דרך קוד יציאה | △ קשה לשלוט בזה | ◎ אתם מחליטים בעצמכם | PowerShell |
לשמור את רשימת האפליקציות הקנונית במקום אחד
המסקנה היא: מה שנכנס לתחום של winget הולך ל-Configuration, ומה שלא — ל-PowerShell.
אבל אם משתמשים בשניהם, שומרים את רשימת האפליקציות באחד מהם בלבד. אם כותבים את החבילות ל-YAML של Configuration וגם כותבים את אותן חבילות ל-wingetPackages שב-kitting.config.json מפרק 5, תיקון של האחד משאיר את השני מיושן.
אם הולכים על מבנה שמשלב Configuration, הגישה המעשית היא להוציא את התקנת האפליקציות מהצד של PowerShell ולתת ל-JSON להחזיק רק את הפריטים ש-Configuration לא מטפל בהם, כמו ערכי Registry ומדפסות משותפות. לעומת זאת, חיפוש אחר resources זמינים בניסיון לכסות הכול ב-Configuration בלבד הוא עבודה שלעיתים קרובות לא מחזירה את ההשקעה.
5. להשלים ב-PowerShell — החלקים ש-winget לא עושה
רוב המאמץ ב-kitting אמיתי הוא בעצם כל מה שאינו התקנת אפליקציות. כאן נכנס PowerShell. המפתח הוא לכתוב הכול כך שלא ייעשה דבר אם הוא כבר נעשה.
5.1. שני שלבים ברמות הרשאה שונות קוראים את קובץ ה-JSON של ההגדרה
הפרק הזה ארוך, אז הנה קודם התמונה הכוללת. יש כאן שלושה קבצים בלבד: קובץ JSON אחד שמחליט מה מתקינים, שאותו קוראים שני סקריפטים שרצים ברמות הרשאה שונות.
flowchart TB
CFG["kitting.config.json<br/>ההגדרה של איך המחשב הזה צריך להיות"]
subgraph ADM["שלב המנהל (מוגבר, פעם אחת לכל מחשב)"]
A1["Invoke-KsKitting.ps1"]
A2["תיקיות / Registry ב-HKLM /<br/>תכונות Windows / חבילות winget /<br/>אפליקציות פנימיות (MSI, EXE)"]
A3["הפצת קובץ ה-JSON של ההגדרה תחת ProgramData"]
A1 --> A2
A1 --> A3
end
subgraph USR["שלב המשתמש (לא מוגבר, בכל כניסה של משתמש)"]
U1["Invoke-KsUserKitting.ps1"]
U2["Registry ב-HKCU /<br/>כונני רשת /<br/>מדפסות משותפות"]
U1 --> U2
end
CFG --> A1
A3 -->|"לקרוא מחדש את ההגדרה המופצת"| U1
איור 1: המבנה התלת-חלקי של ה-kitting. שני שלבים ברמות הרשאה שונות קוראים את אותה הגדרה
יש רק עיקרון אחד מאחורי הפיצול. הגדרות שחלות על כל המחשב נכנסות לשלב המנהל; הגדרות שנוצרות פר-משתמש נכנסות לשלב המשתמש. אם מערבבים בין השניים, כפי שמתואר בהמשך, זה תמיד יישבר בצורה של “הכונן שאמור היה להיות ממופה לא נראה”.
איפה להסתכל בסקריפט המנהל
הקוד בהמשך ארוך, אז הנה קודם מה כתוב איפה. סקריפט המנהל מחולק בהערות --- N. ---, ולכן קוראים רק את החלקים שצריך.
| קטע | מה הוא עושה | מי צריך לקרוא אותו |
|---|---|---|
| הפתיחה (setup) | טעינת קובץ ה-JSON של ההגדרה, הפצתו ל-C:\ProgramData, התחלת הלוג עם Start-Transcript |
כולם |
--- 1. תיקיות תקניות --- |
יצירת תיקיות. הדוגמה הפשוטה ביותר לכתיבה idempotent | מי שרוצה לדעת איך כותבים קוד idempotent |
--- 2. הגדרות Registry --- |
כתיבה ל-HKLM. הנקודה המרכזית היא להשוות גם את ה-type, לא רק את הערך | מי שרוצה להפיץ הגדרות תקן פנימיות |
--- 3. תכונות Windows --- |
הפעלת תכונות, ואיך לאסוף אם נדרשת הפעלה מחדש | מי שצריך .NET Framework 3.5 וכדומה |
--- 4. חבילות winget --- |
החלטה אם חבילה כבר מותקנת, וכתיבה בלי לסמוך על קוד היציאה | מי שרוצה להסתכל רק על החלק של winget |
--- 5. אפליקציות פנימיות --- |
הרצה שקטה של MSI/EXE מתיקייה משותפת. השוואת גרסאות וטיפול בקודי יציאה | מי שרוצה להפיץ אפליקציות פנימיות |
| הסיום (טיפול ביציאה) | בחירה בין החזרת 3010 / 1641 / 1 / 0 | מי שמחבר את זה לכלי הפצה כמו Intune |
5.2. קובץ ה-JSON של ההגדרה מחליט מה יוגדר
הסקריפט הזה קורא את kitting.config.json. דוגמה מלאה של קובץ ההגדרה נכללת כ-kitting.config.json בקוד לדוגמה של המאמר (ה-zip בסוף המאמר), אבל הנה המבנה כבר כאן.
{
"folders": [ "C:\\Work", "C:\\KsTools" ],
"registry": [
{ "key": "HKLM:\\SOFTWARE\\Policies\\Microsoft\\Edge",
"name": "HomepageLocation", "value": "https://intra.example.co.jp/", "type": "String" }
],
"userRegistry": [
{ "key": "HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced",
"name": "HideFileExt", "value": 0, "type": "DWord" }
],
"windowsFeatures": [ "NetFx3" ],
"wingetPackages": [ { "id": "Google.Chrome", "scope": "machine" } ],
"internalApps": [
{ "displayName": "KsApp Business System Client",
"installer": "\\\\fileserver\\deploy\\KsApp\\KsAppSetup.msi",
"arguments": "/quiet /norestart INSTALLDIR=\"C:\\Program Files\\KsApp\"",
"version": "3.2.0" }
],
"drives": [ { "letter": "S", "path": "\\\\fileserver\\share" } ],
"printers": [ { "name": "\\\\printserver\\MFP-1F", "connection": "\\\\printserver\\MFP-1F" } ]
}
הטבלה שלמטה מפרטת את התפקיד של כל מפתח ואת השלב שקורא אותו. כל כוונת התכנון נמצאת בטבלה הזאת.
| מפתח | תוכן | השלב שקורא אותו | הערות |
|---|---|---|---|
folders |
נתיבים של התיקיות התקניות ליצירה | מנהל (מוגבר) | לא עושה דבר אם הן כבר קיימות |
registry |
הגדרות תקן פנימיות שנכתבות ל-HKLM | מנהל (מוגבר) | הגדרות מסוג policy, כמו דף הבית של Edge. הן חלות על כל המשתמשים |
userRegistry |
הגדרות פר-משתמש שנכתבות ל-HKCU | משתמש (לא מוגבר) | דברים כמו הצגת סיומות קבצים. כתיבה ל-HKLM לא משפיעה |
windowsFeatures |
תכונות אופציונליות של Windows להפעלה | מנהל (מוגבר) | ייתכן שתידרש הפעלה מחדש |
wingetPackages |
ה-id וה-scope של החבילות להתקנה עם winget |
מנהל (מוגבר) | לבדוק את ה-scope מראש עם winget show --scope (פרק 2) |
internalApps |
אפליקציות פנימיות בתיקייה משותפת (MSI/EXE) | מנהל (מוגבר) | ה-displayName חייב להתאים בדיוק לשם שמוצג ב-Programs and Features |
drives |
מיפויי כונני רשת | משתמש (לא מוגבר) | פר session של כניסה. אם נוצרים בצד המוגבר, הם בלתי נראים למשתמש |
printers |
חיבורים למדפסות משותפות | משתמש (לא מוגבר) | כמו למעלה. נוצר חיבור פר-משתמש |
העובדה שבעמודת השלב יש שני ערכים היא כל התכנון של קובץ ההגדרה הזה. כשמוסיפים פריט חדש, קודם מחליטים אם זו הגדרה של המחשב או הגדרה של המשתמש, ואז בוחרים לאיזה מפתח הוא נכנס.
להפריד בין הגדרות HKLM להגדרות HKCU
registry ו-userRegistry נפרדים כי הם חלים במקומות שונים. הגדרה כמו “הצגת סיומות קבצים” ב-File Explorer (HideFileExt) נקראת מה-HKCU הפר-משתמש, ולא מ-policy ב-HKLM. כתיבה ל-HKLM בשלב המנהל לא משנה את התצוגה. הגדרות פר-משתמש כאלה שמים ב-userRegistry ומחילים בשלב הלא-מוגבר שמתואר בהמשך.
לאפליקציות פנימיות לציין את שם התצוגה ואת הגרסה הנדרשת
ה-displayName ב-internalApps חייב להתאים בדיוק לשם שמוצג ב-Programs and Features. אם מציינים version, הסקריפט גם בודק אם מותקנת הגרסה הזאת או גרסה חדשה ממנה (כשהוא מושמט, ההחלטה נשענת רק על התאמת השם).
הדוגמה הזאת מתייחסת ל-JSON כמקור הקנוני
את התקנת האפליקציות עצמה אפשר לעשות גם עם winget import או עם WinGet Configuration מהפרק הקודם, אבל הסקריפט הזה כולל גם התקנה של ה-wingetPackages. החזקת הכול בקובץ הגדרה אחד שמה את ההגדרה של מה שנכנס למחשב במקום אחד, והרצה אחת מכסה את הכול.
ולהפך, אם משאירים בקובץ ההגדרה פריט שהסקריפט אף פעם לא קורא, בסופו של דבר נרשם מחשב שאין בו את הפריט כ”kitting שהצליח”.
5.3. להגדיר את כל המחשב בשלב המנהל
מה שקורה כאן: תיקיות, HKLM, תכונות Windows, התקנת אפליקציות, והפצת קובץ ה-JSON של ההגדרה ששלב המשתמש קורא. כוננים משותפים ומדפסות משותפות מפוצלים לשלב הבא.
בקוד, לפני כל פעולה נבדקים הערך וה-type של ה-Registry, הscope והמצב שאחרי ההתקנה של חבילת ה-winget, והשם והגרסה של האפליקציה הפנימית. ביציאה הוא מחזיר לקורא לא רק הצלחה או כישלון, אלא גם אם נדרשת הפעלה מחדש או שהפעלה מחדש כבר החלה.
#Requires -RunAsAdministrator
[CmdletBinding()]
param(
[string] $ConfigPath = "$PSScriptRoot\kitting.config.json"
)
$ErrorActionPreference = 'Stop'
# Get-Content ב-PowerShell 5.1 קורא לפי ה-ANSI code page כשאין BOM.
# קריאת JSON ב-UTF-8 כך במערכת יפנית מובילה ל-mojibake, ולכן מציינים במפורש
$config = Get-Content $ConfigPath -Raw -Encoding utf8 | ConvertFrom-Json
$rebootRequired = $false
$rebootInitiated = $false
$log = "C:\ProgramData\KsKitting\kitting_$(Get-Date -f yyyyMMdd_HHmmss).log"
$null = New-Item -Path (Split-Path $log) -ItemType Directory -Force
# שלב המשתמש שמתואר בהמשך רץ בתהליך נפרד, ולכן מפיצים
# את ההגדרה למקום שכל משתמש יכול לקרוא.
# אם שמים את הקבצים המופצים כאן ומריצים אותם מכאן, מקור ההעתקה והיעד
# הם אותו קובץ. Copy-Item מתייחס להעתקה של קובץ על עצמו כשגיאה, ולכן תחת
# $ErrorActionPreference = 'Stop' כל הסקריפט נעצר עוד לפני שה-kitting מתחיל
$sharedConfig = 'C:\ProgramData\KsKitting\kitting.config.json'
$sourceFull = (Resolve-Path $ConfigPath).ProviderPath
$sharedFull = if (Test-Path $sharedConfig) { (Resolve-Path $sharedConfig).ProviderPath } else { '' }
if (-not [string]::Equals($sourceFull, $sharedFull, 'OrdinalIgnoreCase')) {
Copy-Item $ConfigPath $sharedConfig -Force
}
Start-Transcript -Path $log -Append
try {
# --- 1. תיקיות תקניות ----------------------------------------------
foreach ($dir in $config.folders) {
if (-not (Test-Path $dir)) {
$null = New-Item -Path $dir -ItemType Directory
Write-Verbose "נוצר: $dir"
}
}
# --- 2. הגדרות Registry של תקן פנימי ----------------------------
foreach ($reg in $config.registry) {
if (-not (Test-Path $reg.key)) { $null = New-Item -Path $reg.key -Force }
$item = Get-Item -Path $reg.key
$exists = $item.GetValueNames() -contains $reg.name
# להשוות גם את ה-"type" ולא רק את הערך. REG_SZ "1" ו-DWORD 1 יוצאים
# שווים בהשוואה של PowerShell, מה שמסווג בטעות הגדרה שלא באמת
# נמצאת בתוקף כ"כבר הוחלה"
$sameValue = $exists -and $item.GetValue($reg.name) -eq $reg.value
$sameKind = $exists -and $item.GetValueKind($reg.name).ToString() -eq $reg.type
if (-not ($sameValue -and $sameKind)) {
Set-ItemProperty -Path $reg.key -Name $reg.name -Value $reg.value -Type $reg.type
Write-Verbose "הוגדר: $($reg.key)\$($reg.name) = $($reg.value) ($($reg.type))"
}
}
# --- 3. תכונות Windows -----------------------------------------------
foreach ($feature in $config.windowsFeatures) {
$state = Get-WindowsOptionalFeature -Online -FeatureName $feature
if ($state.State -ne 'Enabled') {
# כש-NoRestart מדכא את ההפעלה מחדש, המידע אם היא נדרשת מופיע
# ב-RestartNeeded שעל ערך ההחזרה. בלי לאסוף אותו, הסקריפט יוצא עם 0
# למרות שנדרשת הפעלה מחדש
$result = Enable-WindowsOptionalFeature -Online -FeatureName $feature -NoRestart
if ($result.RestartNeeded) { $rebootRequired = $true }
}
}
# --- 4. חבילות winget ------------------------------------------------
# קוד היציאה של winget יכול להיות שונה מאפס גם כשהחבילה כבר מותקנת,
# ולהפך — 0 כשהיא לא באמת מותקנת. את ההצלחה קובעים לפי שאילתת המצב.
# בלי --scope הוא תופס את ההתקנה הפר-משתמש של המנהל עצמו
# ומסווג אותה בטעות כ"מותקן ב-machine scope"
function Test-KsWingetPackage {
param([string] $Id, [string] $Scope)
$arguments = @('list', '--id', $Id, '--exact', '--accept-source-agreements')
if ($Scope) { $arguments += @('--scope', $Scope) }
$null = winget @arguments 2>&1
return ($LASTEXITCODE -eq 0)
}
foreach ($package in $config.wingetPackages) {
if (Test-KsWingetPackage -Id $package.id -Scope $package.scope) { continue }
# את ה-backtick של המשך השורה שמים בסוף השורה. הערה שאחריו שוברת את המשך השורה
winget install --id $package.id -e --silent `
--accept-package-agreements --accept-source-agreements `
--scope $package.scope
$wingetExit = $LASTEXITCODE
# אם רק מפיקים אזהרה וממשיכים, כלי ההפצה רושם
# מחשב בלי אפליקציה נדרשת כ"kitting שהצליח"
if (-not (Test-KsWingetPackage -Id $package.id -Scope $package.scope)) {
throw "ההתקנה של $($package.id) נכשלה (winget ExitCode=$wingetExit)"
}
}
# --- 5. אפליקציות פנימיות (הרצה שקטה של installer מתיקייה משותפת) ----
# פותחים את רשימת האפליקציות המותקנות כשמציינים במפורש גם את תצוגת ה-32-bit וגם את זו של 64-bit.
# כש-PowerShell של 32-bit רץ על Windows של 64-bit (מה שקורה בחלק מתצורות Intune),
# ה-WOW64 redirection גורם ל-HKLM:\SOFTWARE\... להצביע על תצוגת ה-32-bit, ואז אפליקציית
# 64-bit מסווגת בטעות כ"לא מותקנת" ומותקנת מחדש בכל פעם
$installedApps = foreach ($view in 'Registry64', 'Registry32') {
$baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey('LocalMachine', $view)
try {
$uninstall = $baseKey.OpenSubKey('SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall')
if (-not $uninstall) { continue }
try {
foreach ($name in $uninstall.GetSubKeyNames()) {
$appKey = $uninstall.OpenSubKey($name)
if (-not $appKey) { continue }
try {
$displayName = $appKey.GetValue('DisplayName')
if ($displayName) {
[pscustomobject]@{
DisplayName = [string] $displayName
DisplayVersion = [string] $appKey.GetValue('DisplayVersion')
}
}
}
finally { $appKey.Dispose() }
}
}
finally { $uninstall.Dispose() }
}
finally { $baseKey.Dispose() }
}
foreach ($app in $config.internalApps) {
$installed = $installedApps | Where-Object DisplayName -eq $app.displayName
# לא להחליט "כבר מותקן" רק לפי התאמת DisplayName. מחשב שנשאר עם
# גרסה ישנה אף פעם לא יקבל את החדשה, והרצה חוזרת לא מתכנסת למצב
# שבקובץ ההגדרה. אם בהגדרה יש version, בודקים אם מותקנת הגרסה הזאת או גרסה חדשה ממנה
$upToDate = if ($app.version) {
$wanted = [version] $app.version
[bool]($installed | Where-Object {
$cur = $_.DisplayVersion -as [version] # צורות שאינן בפורמט version מוחרגות
$cur -and $cur -ge $wanted
})
} else {
[bool] $installed
}
if ($upToDate) { continue }
# ה-ArgumentList של Start-Process מחבר את המערך עם רווחים לשורת פקודה אחת, ולכן
# כל ערך שמכיל רווח צריך להיות במרכאות בצד של קובץ ההגדרה
# דוגמה: '/quiet /norestart INSTALLDIR="C:\Program Files\KsApp"'
# קובץ .msi אינו executable, ולכן העברה ישירה שלו ל-Start-Process נכשלת עם
# "is not a valid application." מריצים אותו דרך msiexec
if ([System.IO.Path]::GetExtension($app.installer) -eq '.msi') {
$msiArgs = @('/i', "`"$($app.installer)`"") + $app.arguments
$proc = Start-Process -FilePath 'msiexec.exe' -ArgumentList $msiArgs -Wait -PassThru -NoNewWindow
}
else {
$proc = Start-Process -FilePath $app.installer -ArgumentList $app.arguments -Wait -PassThru -NoNewWindow
}
# בקודי היציאה של Windows Installer, 0 הוא לא ההצלחה היחידה.
# גם 3010 וגם 1641 הם הצלחה; ההבדל הוא רק באופן הטיפול בהפעלה מחדש
switch ($proc.ExitCode) {
0 { } # הצלחה
3010 { $rebootRequired = $true } # הצלחה. נדרשת הפעלה מחדש (ERROR_SUCCESS_REBOOT_REQUIRED)
1641 { $rebootInitiated = $true } # הצלחה. ה-installer התחיל הפעלה מחדש
default {
throw "ההתקנה של $($app.displayName) נכשלה (ExitCode=$($proc.ExitCode))"
}
}
# אם הפעלה מחדש כבר החלה, גם ככה כל התקנה נוספת תיקטע
if ($rebootInitiated) { break }
}
if ($rebootInitiated) {
# 1641 אומר "הצלחה, אבל הפעלה מחדש כבר החלה". החזרת 0 גורמת
# לכלי ההפצה לפרש את זה כ"הסתיים ואז הופעל מחדש מעצמו", ולכן מעבירים את הקוד הלאה
Write-Host 'ההתקנה החלה הפעלה מחדש. הריצו שוב אחרי ההפעלה מחדש' -ForegroundColor Yellow
exit 1641
}
if ($rebootRequired) {
# החזרת 3010 כמו שהוא מאפשר ל-Intune או לכלי ההפצה לפרש את זה כ"הצלחה,
# נדרשת הפעלה מחדש", לתזמן את ההפעלה ולדווח עליה. החזרת 0 כאן משמעותה שההפעלה מחדש נשכחת
Write-Host 'ה-kitting הושלם (נדרשת הפעלה מחדש)' -ForegroundColor Yellow
exit 3010
}
Write-Host 'ה-kitting הושלם' -ForegroundColor Green
exit 0
}
catch {
Write-Warning "כשל: $($_.Exception.Message)"
Write-Warning $_.InvocationInfo.PositionMessage
exit 1
}
finally {
Stop-Transcript
}
איך לקרוא את קודי היציאה של סקריפט המנהל
| קוד מוחזר | מה הוא אומר בסקריפט הזה | מה עושים הלאה |
|---|---|---|
0 |
הסתיים ולא מבקש הפעלה מחדש | לרשום כהושלם |
3010 |
הצלחה, אבל נדרשת הפעלה מחדש כדי שהשינויים ייכנסו לתוקף | להודיע לכלי ההפצה שנדרשת הפעלה מחדש |
1641 |
הצלחה. ה-installer התחיל הפעלה מחדש | לא להמשיך; להריץ שוב אחרי ההפעלה מחדש |
1 |
הסקריפט תפס שגיאה | לבדוק בלוג איפה זה נכשל |
3010 ו-1641 של Windows Installer אינם כשלים. ולהפך, החזרה של 0 בלבד לקורא לא משאירה שום דרך לתקשר שנדרשת הפעלה מחדש או שהיא כבר החלה.6
5.4. להריץ את שלב המשתמש ללא הרשאות מוגברות, בכניסה של המשתמש
שימו לב שמיפוי כונני רשת הושאר בכוונה מחוץ לסקריפט המנהל הזה. מיפוי אות כונן הוא הגדרה פר-session של כניסה, ולכן מיפוי שנוצר ב-session שהוגבר להרשאות מנהל לא נראה מ-File Explorer הרגיל של המשתמש תחת UAC.
אם מריצים את זה בהרשאות מערכת מכלי כמו Intune, המיפוי נוצר מלכתחילה ב-session של SYSTEM, וזה חסר תועלת למשתמש. התשובה הנכונה היא להחיל הגדרות פר-משתמש ללא הרשאות מוגברות, בכניסה של אותו משתמש.
# הגדרות פר-משתמש (להריץ ללא הרשאות מוגברות, בכניסה של המשתמש. איך לרשום את זה מתואר בהמשך)
# זהו תהליך נפרד מסקריפט המנהל, ולכן $config לא עובר בירושה.
# קוראים מחדש את ההגדרה ששלב המנהל הפיץ ל-ProgramData
$configPath = 'C:\ProgramData\KsKitting\kitting.config.json'
if (-not (Test-Path $configPath)) {
Write-Warning "קובץ ההגדרה לא נמצא: $configPath"
exit 1
}
$config = Get-Content $configPath -Raw -Encoding utf8 | ConvertFrom-Json
# הגדרות Registry פר-משתמש (HideFileExt וכדומה. כתיבה ל-HKLM לא משפיעה)
foreach ($reg in $config.userRegistry) {
if (-not (Test-Path $reg.key)) { $null = New-Item -Path $reg.key -Force }
$item = Get-Item -Path $reg.key
$exists = $item.GetValueNames() -contains $reg.name
$same = $exists -and
$item.GetValue($reg.name) -eq $reg.value -and
$item.GetValueKind($reg.name).ToString() -eq $reg.type
if (-not $same) {
Set-ItemProperty -Path $reg.key -Name $reg.name -Value $reg.value -Type $reg.type
}
}
# כונני רשת
foreach ($drive in $config.drives) {
$local = "$($drive.letter):"
$existing = Get-SmbMapping -LocalPath $local -ErrorAction SilentlyContinue
if ($existing) {
# החלטה רק לפי "האם יש מיפוי?" אומרת שכשהשיתוף עובר ואתם משנים
# את ההגדרה, המיפוי הישן נשאר וזה עדיין מדווח כ"הצלחה". משווים גם את היעד
if ($existing.RemotePath.TrimEnd('\') -eq $drive.path.TrimEnd('\')) { continue }
Remove-SmbMapping -LocalPath $local -Force
}
New-SmbMapping -LocalPath $local -RemotePath $drive.path -Persistent $true
}
# גם החיבורים למדפסות משותפות הם פר-משתמש. אם מריצים כמנהל או כ-SYSTEM
# החיבור נוצר רק עבור אותו חשבון, ולא נראה למשתמש
foreach ($printer in $config.printers) {
if (-not (Get-Printer -Name $printer.name -ErrorAction SilentlyContinue)) {
Add-Printer -ConnectionName $printer.connection
}
}
גם מדפסות, HKCU והפרופיל שייכים לצד המשתמש
חיבור למדפסת משותפת (Add-Printer -ConnectionName) מטופל באותה דרך. זו פעולה שיוצרת חיבור פר-משתמש, ולכן ביצוע שלה בסקריפט המנהל או בהרצה של Intune כ-SYSTEM משאיר אותה בלתי נראית לעובד שייכנס אחר כך. אם רוצים שהיא תהיה קיימת בכל מחשב, או להשתמש במנגנון שפורס מדפסות פר-מחשב, כמו הפצת policy משרת הדפסה, או להריץ אותה בשלב הלא-מוגבר הזה.
מאותה סיבה, הנחת קבצים תחת פרופיל המשתמש, כתיבה ל-HKCU ויצירת קיצורי דרך עבור המשתמש — כל אלה שייכים לשלב הלא-מוגבר הזה. הצורה הדו-שלבית, הגדרות ברמת המחשב פעם אחת כמנהל והגדרות פר-משתמש ללא הרשאות מוגברות בכל כניסה, היא הצורה הבסיסית של סקריפט kitting. לאזהרות סביב נתיבי UNC ומיפויי כוננים ראו גם “מלכודות של כונני רשת ונתיבי UNC”.
5.5. לבחור דרך אחת להפעיל את זה בכניסה
העיקר בתכנון הדו-שלבי הוא המנגנון שמריץ את הסקריפט הלא-מוגבר הזה בכניסה של המשתמש, תחת ההרשאות של אותו משתמש. אם זה לא מחובר, רץ רק שלב המנהל ובסופו של דבר מתקבל מחשב שבו גם הכוננים וגם המדפסות לא מוגדרים. יש שלוש דרכים לעשות זאת.
| שיטה | מתי זה רץ | לאילו סביבות מתאים | אזהרות |
|---|---|---|---|
| מפתח ה-Run ב-HKLM | בכל כניסה של כל משתמש | לא מחובר ל-domain. רוצים שסקריפט אחד מ-Intune או מכלי הפצה יכסה הכול | הוא רץ בכל פעם, ולכן idempotency הוא תנאי הכרחי. צריך אפשרות שמונעת הופעה של חלון |
| Task Scheduler (טריגר logon) | בכל כניסה של כל משתמש | רוצים היסטוריה של תוצאות ההרצה. רוצים התחלה מושהית | לכבות את “Run with highest privileges” (הפעלה שלו מגבירה את ההרשאות של ה-task ומאיינת את המטרה) |
| סקריפט logon (Group Policy) | בכל כניסה של כל משתמש | מחובר ל-domain | משתלב בתפעול ה-GPO הקיים, אבל מסורבל למחשב שעומד לבד |
שיטה 1: לרשום אותו במפתח ה-Run ב-HKLM
רושמים אותו בהרשאות מנהל. אם משלבים אותו בסקריפט המנהל, שמים אותו לפני ה-exit שמחזיר את קוד היציאה. קובץ ה-JSON של ההגדרה ששלב המשתמש קורא מופץ על ידי שלב המנהל בסעיף 5.3.
# לשים את הסקריפט של שלב המשתמש במקום שכל משתמש יכול לקרוא
$userScript = 'C:\ProgramData\KsKitting\Invoke-KsUserKitting.ps1'
Copy-Item "$PSScriptRoot\Invoke-KsUserKitting.ps1" $userScript -Force
# מפתח ה-Run ב-HKLM רץ בהרשאות של המשתמש שהתחבר (ללא הגברת הרשאות).
# ניסיון לכתוב במקום זה ל-HKCU יכתוב, מכיוון שזה רץ כמנהל או כ-SYSTEM,
# ל"HKCU של המנהל עצמו", ולא ל-HKCU של העובד שעומד להשתמש במחשב
$runKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'
$command = 'powershell.exe -NoProfile -ExecutionPolicy Bypass ' +
"-WindowStyle Hidden -File `"$userScript`""
Set-ItemProperty -Path $runKey -Name 'KsUserKitting' -Value $command
שיטה 2: לרשום task מתוזמן (כשרוצים היסטוריה)
למקרה שדילגתם על שיטה 1 והתחלתם לקרוא מכאן — זה חוזר על הנחת הסקריפט מההתחלה.
# לשים אותו במקום שכל משתמש יכול לקרוא (כמו בשיטה 1)
$userScript = 'C:\ProgramData\KsKitting\Invoke-KsUserKitting.ps1'
New-Item -ItemType Directory -Path (Split-Path $userScript) -Force | Out-Null
Copy-Item -Path '.\Invoke-KsUserKitting.ps1' -Destination $userScript -Force
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
-Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$userScript`""
$trigger = New-ScheduledTaskTrigger -AtLogOn
# ציון BUILTIN\Users (S-1-5-32-545) גורם לו לרוץ בהרשאות של מי שהתחבר.
# RunLevel Limited הוא ההגדרה של "לא להגביר הרשאות". אם מגדירים Highest, ה-task רץ
# ב-session מוגבר, ומיפויי הכוננים מפסיקים להיות נראים למשתמש
$principal = New-ScheduledTaskPrincipal -GroupId 'S-1-5-32-545' -RunLevel Limited
Register-ScheduledTask -TaskName 'KsUserKitting' `
-Action $action -Trigger $trigger -Principal $principal -Force
שיטה 3: סקריפט logon של Group Policy
ההגדרה נמצאת ב-User Configuration > Policies > Windows Settings > Scripts (Logon/Logoff) > Logon (ב-gpedit.msc של מחשב שעומד לבד אין רמת Policies, ולכן זה User Configuration > Windows Settings > Scripts (Logon/Logoff)). מוסיפים אותה בלשונית PowerShell Scripts של הדיאלוג. כתיבת .ps1 ישירות בלשונית Scripts גורמת לו להיות מטופל כ-executable, וזה לא מתנהג כפי שהתכוונתם. ב-policy מקומי, הסקריפט עצמו מונח ב-%SystemRoot%\System32\GroupPolicy\User\Scripts\Logon.
מכיוון שזה רץ בכל פעם, בודקים את המצב הנוכחי
בכל אחת מהשיטות, העובדה שהוא רץ בכל כניסה זהה. זה חלק מהסיבה שהסקריפט של שלב המשתמש נכתב באופן idempotent, כלומר בודק את המצב הנוכחי לפני שהוא משנה. אם רוצים שהוא ירוץ פעם אחת בלבד, כותבים סמן השלמה תחת HKCU ובודקים אותו בראש הסקריפט.
5.6. לסקור הרצות חוזרות, תיעוד ביקורת ואינטגרציה עם כלי ההפצה
לעבור על התכנון עד כאן לפני שמריצים אותו בתפעול.
ליישר הרשאות, הגדרות והמצב הנוכחי
- להפריד הגדרות שדורשות הגברת הרשאות מהגדרות פר-משתמש. כמו למעלה, ערבוב שלהן נשבר בצורה של “הכונן שאמור היה להיות ממופה לא נראה”
- להוציא את ההגדרות ל-JSON. כך אפשר לבטא הבדלים לפי מחלקה ולפי דגם מחשב בלי לשנות את הסקריפט
- לבדוק את המצב הנוכחי לפני כל פעולה. זה מה שהופך את זה לבטוח להרצה כמה פעמים שרוצים
לשמור לוג של מה שנעשה
לשמור תיעוד ביקורת עם Start-Transcript. אחר כך אפשר לעקוב אחרי מה נעשה במחשב נתון (“זרמי הפלט של PowerShell ותכנון הלוגים”)
לתקשר את הצורך בהפעלה מחדש גם דרך קוד היציאה
להחזיר קוד יציאה. כך Intune או כלי הפצה יכולים להחליט אם זה הצלחה או כישלון. שימו לב שב-Windows Installer 0 הוא לא ההצלחה היחידה. גם 3010 (ERROR_SUCCESS_REBOOT_REQUIRED, נדרשת הפעלה מחדש) וגם 1641 (ERROR_SUCCESS_REBOOT_INITIATED, הפעלה מחדש כבר החלה) הם הצלחות.6 אם הם נופלים ל-default ומטופלים ככשלים, התקנה מוצלחת נרשמת באדום בכלי ההפצה. אם מתייחסים אליהם כהצלחה, המפתח הוא להחזיר את אותו קוד לקורא בסוף. אם מחזירים 0, כלי ההפצה מאבד כל דרך לדעת שנדרשה הפעלה מחדש (“טיפול בשגיאות ו-retry ב-PowerShell”)
לארגומנטים שמכילים רווחים — לבדוק את המרכאות
שימו לב למרכאות בארגומנטים של installer. Start-Process -ArgumentList רק מחבר את המערך עם רווחים, ולכן גבולות הארגומנטים לא נשמרים. או שמציבים מרכאות סביב נתיבים שמכילים רווחים בצד של קובץ ההגדרה, או שמשתמשים ב-ProcessStartInfo.ArgumentList (PowerShell 7) (“לקרוא נכון ל-exe חיצוני מ-PowerShell”)
6. שימוש ב-winget ממודול PowerShell
לעבוד עם אובייקטים, לא עם מחרוזות
שימוש במודול PowerShell עמיד יותר מאשר לפרסר את הפלט של שורת הפקודה כמחרוזות. Microsoft.WinGet.Client מספק cmdlets כמו Find-WinGetPackage / Install-WinGetPackage / Get-WinGetPackage / Update-WinGetPackage.4
Install-Module -Name Microsoft.WinGet.Client -Scope AllUsers -Force
# לבדוק אם זה מותקן לפני ההתקנה (idempotent)
foreach ($id in 'Google.Chrome', 'Microsoft.VisualStudioCode', '7zip.7zip') {
if (Get-WinGetPackage -Id $id -ErrorAction SilentlyContinue) {
Write-Verbose "כבר מותקן: $id"
continue
}
Install-WinGetPackage -Id $id -Mode Silent -Scope System
}
מכיוון שהתוצאות חוזרות כאובייקטים, אפשר לכתוב ישירות בדיקות הצלחה ולהצליב רשימות, וזה היתרון.
7. אזהרות להרצה לא-מאוישת ול-system context
כל ניסיון לאוטמט את ה-kitting לגמרי נתקל בשאלה תחת איזה חשבון הוא רץ. גם אחרי בדיקות ה-scope בפרק 2 ופיצול השלבים בפרק 5, הדבר האחרון שצריך הוא לאמת עם אותו חשבון הרצה שמשתמשים בו בזמן ההפצה.
לא להניח שזה עובד תחת SYSTEM
- חלקים מ-winget מניחים הרצה ב-user context. הרצה ב-system context היא עדיין בשלב שבו Microsoft רושמת אותה כתכונה עתידית3
לא לבלבל בין מה שעבד כמנהל לבין הסביבה של המשתמש
- להפריד עבודה שדורשת הגברת הרשאות מעבודה פר-משתמש. מבנה שמחיל הגדרות ברמת המחשב בהרשאות מנהל והגדרות תחת פרופיל המשתמש בכניסה הראשונה נוח יותר לתפעול
לאמת באותם תנאים כמו בהפצה
- תמיד לאמת עם חשבון ההרצה שבאמת ישמש. “זה עבד בחשבון המנהל שלי אבל לא אחרי ההפצה” הוא הכשל הנפוץ ביותר בתחום הזה (“כשה-task של Task Scheduler לא רץ”)
8. כללי אצבע מעשיים (טבלת החלטות)
לסיום, מפילים כל פריט בספר הריצה על הטבלה הזאת. אם מחליטים לא רק על אמצעי ההרצה אלא גם על ההרשאות, ההרצות החוזרות, התיעוד וההפעלה מחדש, ה-kitting הופך למשהו שאפשר לבנות כתהליך תפעולי.
| מה עושים | אמצעי | הערות |
|---|---|---|
| התקנת מוצרים מסחריים ומוצרי קוד פתוח | winget install / configure | --silent --accept-* הם חובה1 |
| חילוץ התצורה ממחשב תקני קיים | winget export |
ההגדרות לא נכללות. להשתמש בזה כנקודת התחלה2 |
| אפליקציות + הגדרות Windows, באופן הצהרתי | winget configure (YAML) |
Windows 10 1809 ומעלה + winget 1.6 ומעלה3 |
| Registry, תכונות Windows, אפליקציות פנימיות | PowerShell (מנהל) | לבדוק את המצב לפני שמשנים (idempotent) |
| כוננים משותפים, מדפסות משותפות, הגדרות פר-משתמש | PowerShell (בכניסה, ללא הגברת הרשאות) | אם נוצרים מוגברים או כ-SYSTEM, הם בלתי נראים למשתמש |
| אפליקציות פנימיות | PowerShell + installer שקט | בניית מאגר ייעודי היא יותר מדי בקנה מידה קטן |
| שליטה בזה מ-PowerShell | Microsoft.WinGet.Client |
מייתר את הצורך לפרסר את הפלט כמחרוזות4 |
| הרצה לא-מאוישת | לאמת עם חשבון ההרצה | ל-system context יש מגבלות3 |
| תיעוד מה שנעשה | Start-Transcript + קוד יציאה |
שומר תיעוד של מה נעשה בכל מחשב |
| כשנדרשת הפעלה מחדש | להחזיר קוד יציאה 3010 / 1641 | שניהם הצלחה. אם מחזירים 0, כלי ההפצה לא מזהה את ההפעלה מחדש6 |
9. סיכום
- אפשר להחליף ספר ריצה של kitting בקבצים שאפשר להריץ. להשאיר את התקנת האפליקציות ל-winget ואת כל השאר ל-PowerShell — זו החלוקה הריאלית.
- ב-
winget installתמיד להוסיף--silentו---accept-package-agreements--accept-source-agreements. שוכחים אותם וההרצה הלא-מאוישת נעצרת. winget exportיכול לחלץ את התצורה ממחשב תקני קיים, אבל הגדרות ואפליקציות שמחוץ לניהול של winget לא נכללות.- עם WinGet Configuration (
winget configure) אפשר לכנס אפליקציות והגדרות לקובץ הצהרתי אחד, ולקבל מבנה שעמיד להרצות חוזרות. - תמיד לכתוב את הצד של PowerShell כך שיבדוק את המצב הנוכחי לפני שהוא משנה, כדי להבטיח idempotency.
- לפצל את שלבי ההרצה: הגדרות ברמת המחשב בהרשאות מנהל, והגדרות פר-משתמש כמו כונני רשת ומדפסות משותפות ללא הגברת הרשאות בכניסה. חיבורים שנוצרו ב-session מוגבר או כ-SYSTEM לא נראים למשתמש.
- בהרצה לא-מאוישת, אימות חשבון ההרצה הוא הדבר החשוב ביותר. לתכנן מתוך הנחה שלריצת winget ב-system context יש מגבלות.
הורדת הקוד לדוגמה
הקוד שנדון במאמר הזה מופץ ארוז בצורה שאפשר להריץ כמו שהוא. הוא מכיל דוגמאות מלאות של שלב המנהל, שלב המשתמש וקובץ ההגדרה.
מכיוון שהדוגמאות במאמר הזה תלויות ב-Windows וב-tenant, הן לא אומתו בהרצה. על כל קובץ בוצעו ניתוח תחביר וניתוח סטטי עם PSScriptAnalyzer, אבל תמיד לאמת את ההתנהגות על מכונת הבדיקה שלכם.
# ניתוח תחביר + ניתוח סטטי (רץ גם מחוץ ל-Windows)
./Invoke-SampleTests.ps1
הערכים המוגדרים (נתיבים, שמות שרתים, מזהי tenant וכדומה) הם דוגמה. אל תריצו אותם כמו שהם ב-production; התאימו אותם לסביבה שלכם.
מאמרים קשורים
- לקרוא נכון ל-exe חיצוני מ-PowerShell — מלכודות של מרכאות בארגומנטים, קודי יציאה ו-mojibake
- טיפול בשגיאות ו-retry ב-PowerShell — ממלכודת ה-try/catch ועד קודי יציאה ושיטות retry מקובלות
- להפסיק להשתמש ב-Write-Host — זרמי הפלט של PowerShell ותכנון הלוגים
- כשה-task של Task Scheduler לא רץ או מסתיים עם 0x1 — בידוד הסיבה ותכנון לתפעול אמין
- לקבע מסופי עבודה עם kiosk mode — איך לבחור בין Assigned Access ל-Shell Launcher ולתכנן את התפעול
- הפתרון המעשי אחרי סיום התמיכה ב-Windows 10 — טבלת החלטות ל-ESU, LTSC והחלפה
תחומי ייעוץ קשורים
KomuraSoft LLC עוסקת ב-kitting של מחשבים, באוטומציה של סביבות תקן פנימיות, בהפיכת תפעול שכלוא בספר ריצה ובראש של אדם אחד למשהו שאפשר להריץ, ובתמיכה בתכנון של סקריפטים להפצה.
מקורות
-
Microsoft Learn, install command (winget). על ציון היעד עם –id / -e, על התקנה לא-מאוישת עם –silent, על הסכמה לתנאי השימוש עם –accept-package-agreements / –accept-source-agreements, ועל ציון טווח ההתקנה (user / machine) עם –scope. ראו גם את רשימת הפקודות ב-Use WinGet to install and manage applications. ↩ ↩2 ↩3
-
Microsoft Learn, export command (winget). על כך שאפשר לכתוב את רשימת החבילות המותקנות ל-JSON, על רישום הגרסאות עם –include-versions, על שחזור עם import command ועל ההתנהגות של –ignore-unavailable, על כך שהייצוא מוגבל לחבילות שנמצאות בניהול של winget, ועל ההיררכיה של ה-JSON שיוצא (Sources / Packages / PackageIdentifier / Version, כש-Version הוא אופציונלי). מבנה ה-JSON מוגדר גם ב-packages.schema.2.0.json: הרמה העליונה מחזיקה WinGetVersion, CreationDate ו-Sources, כל איבר ב-Sources מחזיק SourceDetails (Name / Identifier / Argument / Type) ו-Packages, וכל איבר ב-Packages מחייב PackageIdentifier ומחזיק באופן אופציונלי Version ועוד. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, WinGet Configuration. על כך ש-WinGet Configuration הוא מנגנון שמצהיר על המצב הרצוי ב-YAML ומחיל אותו באמצעות PowerShell DSC, על כך שאפשר להשתמש בו ל-setup לא-מאוישת, על כך שהוא דורש Windows 10 גרסה 1809 (build 17763) ומעלה או Windows 11 ו-WinGet v1.6.2631 ומעלה, על איך מטופל UAC כשמריצים אותו מ-shell של מנהל, ועל כך שהרצה ב-system context רשומה כפריט פיתוח עתידי. ראו גם את show ואת –accept-configuration-agreements ב-configure command. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
GitHub, microsoft/winget-cli — Microsoft.WinGet.Client PowerShell module. על כך שאפשר להתקין את המודול Microsoft.WinGet.Client מ-PowerShell Gallery, ועל ה-cmdlets שהוא מספק, כמו Find-WinGetPackage / Get-WinGetPackage / Install-WinGetPackage / Update-WinGetPackage / Uninstall-WinGetPackage, שמאפשרים לעבוד עם התוצאות כאובייקטים. ↩ ↩2 ↩3
-
Microsoft Learn, show command (winget). על כך שזו הפקודה שמציגה את הפרטים של אפליקציה שצוינה (מטא-דאטה ומידע על ה-installer), על כך שאפשרות –scope מאפשרת לבחור את טווח ההתקנה (user / machine), ועל כך שמידע ה-installer שמוצג מבוסס על הארגומנטים שציינתם ועל ההחלטה של WinGet עצמו. ↩
-
Microsoft Learn, Windows Installer Error Codes. על כך ש-ERROR_SUCCESS_REBOOT_REQUIRED (3010) אומר “נדרשת הפעלה מחדש כדי שהשינויים ייכנסו לתוקף; ההתקנה עצמה הצליחה”, ועל כך ש-ERROR_SUCCESS_REBOOT_INITIATED (1641) אומר “ה-installer התחיל הפעלה מחדש” — קוד שמסמן הצלחה. ↩ ↩2 ↩3
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
סדר name resolution ב-Windows — hosts, DNS cache, LLMNR/mDNS ו-DoH
האם hosts, ה-DNS cache, שרת DNS או LLMNR/mDNS ענו קובע למה חלק מהמחשבים נכשלים. לומדים את סדר name resolution ב-Windows, מה DoH משנה, ואי...
מה Fast Startup באמת עושה — למה "Shut down" ב-Windows אינו אותו דבר כמו Restart
Shutdown ב-Windows הוא hybrid shutdown כברירת מחדל, ושומר את ה-kernel ואת ה-drivers ל-hiberfil.sys. למה רק restart מאפס אותם, ומתי לכבות ...
ניהול תצורה הצהרתי של Windows עם DSC — מתחילים IaC עם dsc.exe
יוצאים מסקריפטי runbook שנשברים בהרצה שנייה. DSC v3 (dsc.exe) מצהיר על תצורת Windows ב-YAML ומחיל אותה באופן אידמפוטנטי. המאמר מסדר את אר...
SMB signing ו-LDAP channel binding — לסגור בעבודה המעשית את "החצי השני" של הגנת NTLM
SMB signing ו-LDAP signing/channel binding מגבילים את נזקי ה-relay עד שמסיימים עם NTLM. המאמר מפרט את ברירות המחדל לפי OS, איך קוראים את ...
האם הוצאת NTLM משימוש תעצור את אפליקציות העסק? — איך לאסוף לוגי ביקורת ובאיזה סדר לחסל תלויות
המאמר מסביר איך מאתרים היכן Windows ואפליקציות עסקיות תלויות ב-NTLM: מדיניות ביקורת, אירועים 8001-8004 ביומן NTLM/Operational, הסיבות והת...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
פיתוח יישומי Windows
יישומים עסקיים, חיבור התקנים וכלי תקשורת, מהגדרת הדרישות ועד הפיתוח.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- האם winget import יכול לאוטמט את כל ה-kitting של המחשב?
- הוא יכול לאוטמט את התקנת האפליקציות, אבל זה לבדו לא מספיק. מה ש-winget import משחזר הוא רשימת חבילות; הגדרות בתוך האפליקציה, הוספת מדפסות, מיפוי כונני רשת, הגדרות צריכת חשמל, הגדרות תקן פנימיות דרך ה-Registry וכן הלאה — כל אלה מחוץ לתחום. גם אפליקציות שהותקנו בדרך אחרת מלבד winget, וגם אפליקציות עסקיות פנימיות, אינן נכללות בייצוא. בפועל, הגישה הריאלית היא מבנה דו-שלבי: להשאיר את התקנת האפליקציות ל-winget ולהשלים את שאר ההגדרות בסקריפט PowerShell.
- מה ההבדל בין winget לבין WinGet Configuration (winget configure)?
- install/import של winget הם הוראות פרוצדורליות בסגנון "תתקין את אלה, בסדר הזה", בעוד ש-WinGet Configuration היא דרך לכתוב בקובץ YAML הצהרה בסגנון "זה המצב שאני רוצה להגיע אליו". מבפנים היא משתמשת ב-PowerShell DSC, כך שקובץ אחד יכול לבטא לא רק התקנת אפליקציות אלא גם הגדרות של Windows והגדרות של האפליקציות עצמן. מכיוון שהיא לא עושה דבר כשהמצב הרצוי כבר קיים, אפשר פשוט להריץ שוב את אותו קובץ אם משהו נכשל באמצע, וזה מה שהופך אותה לעמידה כשצריך לחזור על ה-kitting. היא דורשת Windows 10 1809 ומעלה ו-winget 1.6 ומעלה.
- בטוח להריץ את winget בהרשאות SYSTEM מ-Task Scheduler או מ-Intune?
- צריך להיזהר. חלקים מ-winget מניחים הרצה ב-user context, והרצה ב-system context היא עדיין משהו ש-Microsoft רושמת כפריט פיתוח עתידי. בפועל עוקפים את זה עם --scope machine כדי להתקין לכל המשתמשים, דרך מודול PowerShell (Microsoft.WinGet.Client), או בהרצה ב-user context בכניסה הראשונה. כך או כך — תמיד לאמת עם חשבון ההרצה שאיתו הסקריפט באמת ירוץ.
- סקריפט kitting צריך להיות בטוח להרצה כמה פעמים שרוצים?
- כן. את ה-idempotency (קבלת אותה תוצאה לא משנה כמה פעמים הוא רץ) צריך להתייחס כהכרחית. kitting נכשל באמצע באופן שגרתי, ובכל פעם צריך להיות מסוגלים להתחיל מהתחלה. אם כותבים כך שיצירת תיקיות בודקת קיום עם Test-Path קודם, שהגדרות ה-Registry בודקות את הערך הנוכחי קודם, ושהתקנת אפליקציות בודקת אם האפליקציה כבר מותקנת קודם — אפשר להמשיך מהנקודה שבה זה נכשל. WinGet Configuration בנויה מראש סביב דרך החשיבה הזאת.
- אפשר להפיץ אפליקציות עסקיות פנימיות דרך winget?
- אפשר, אם מקימים מאגר פרטי פנימי (source מסוג REST API), אבל זה אומר לבנות ולתחזק שרת בשבילו. עבור קומץ אפליקציות, הרצה שקטה של installer מתיקייה משותפת בסקריפט PowerShell פשוטה יותר. בארגונים קטנים החלוקה הריאלית ביותר היא להשאיר מוצרים מסחריים ומוצרי קוד פתוח ל-winget ולהתקין אפליקציות פנימיות ב-PowerShell.