DSCでサーバー構成管理:プロフェッショナルなPowerShell運用術

Mermaid

DSCでサーバー構成管理:プロフェッショナルなPowerShell運用術

はじめに

DSC (Desired State Configuration) は、Windowsサーバーやクライアントの構成をコードで定義し、その状態を自動的に維持するためのPowerShellの強力な機能です。OSの機能や役割、レジストリ設定、ファイルの状態、サービス、プロセスなど、多岐にわたる項目を構成管理の対象とできます。本記事では、DSCを大規模環境で効率的かつ堅牢に運用するためのPowerShellスクリプトの実装と、現場で直面するであろう課題への対応策を、プロの視点から解説します。

目的と前提 / 設計方針

目的

複数台のWindowsサーバー群に対し、DSC構成を効率的かつ一貫性をもって適用・管理することを目指します。特に、大規模環境におけるプッシュ型DSC適用時の並列処理、エラーハンドリング、スループットの最適化、および可観測性の確保に焦点を当てます。

前提

  • ターゲットノードはWindows Server(またはWindowsクライアント)。
  • ターゲットノードはWinRMが有効であり、接続可能な状態であること。
  • PowerShell 5.1またはPowerShell 7.xが利用可能であること。
  • 本稿ではプッシュ型DSCを中心に解説します。

設計方針(同期/非同期、可観測性)

多数のノードに対してDSC構成を適用する場合、同期的な処理では膨大な時間がかかり、運用に耐えられません。そのため、非同期かつ並列での処理を基本とします。具体的には、PowerShellのジョブ機能 (Start-Job) を活用して、各ノードへのDSC適用を並列実行します。

また、実行中の状況や結果を明確に把握できるよう、詳細なログ出力と進捗表示を実装し、可観測性を高めます。エラー発生時には即座に検知し、適切なリカバリ(再試行)を行うためのメカニズムも組み込みます。

コア実装(並列/キューイング/キャンセル)

ここでは、DSC構成の定義、MOFファイルの生成、そしてターゲットノードへの並列プッシュ適用、タイムアウト、および再試行ロジックの実装について説明します。

sequenceDiagram
    participant Operator as DSC管理者
    participant Orchestrator as PowerShellオーケストレータ
    participant TargetNode1 as ターゲットノード1
    participant TargetNode2 as ターゲットノード2
    participant TargetNodeN as ターゲットノードN

    Operator ->> Orchestrator: DSC構成適用コマンド実行
    Orchestrator ->> Orchestrator: DSC構成スクリプト(.ps1)をMOFにコンパイル
    Orchestrator ->> Orchestrator: ターゲットノードリスト読み込み
    loop 各ターゲットノードに対して
        Orchestrator ->> Orchestrator: ジョブスクリプトブロック生成 (Start-DscConfiguration, Try/Catch, ロギング)
        Orchestrator ->> TargetNode1: Start-JobでDSC適用開始 (非同期)
        Orchestrator ->> TargetNode2: Start-JobでDSC適用開始 (非同期)
        Orchestrator ->> TargetNodeN: ...
    end
    Orchestrator ->> Orchestrator: ジョブの完了を待機 (Wait-Job)
    loop 各ジョブに対して
        Orchestrator ->> Orchestrator: 結果受信 (Receive-Job)
        alt 成功
            Orchestrator ->> Orchestrator: 成功ログ記録
        else 失敗/タイムアウト
            Orchestrator ->> Orchestrator: エラーログ記録
            Orchestrator ->> Orchestrator: 再試行ロジック実行 (もしあれば)
        end
    end
    Orchestrator ->> Orchestrator: 全体結果集計
    Orchestrator ->> Operator: 結果レポート出力

DSC構成の例

まず、ターゲットノードに適用するDSC構成を定義します。ここでは、Web-Server (IIS) 機能をインストールし、特定のサービス(例: Spooler)が実行状態であることを保証する簡単な構成を例示します。

# DSC構成スクリプト例 (ExampleDscConfig.ps1)
Configuration ExampleDscConfig
{
    Param
    (
        [Parameter(Mandatory=$true)]
        [string[]]$NodeName
    )

    Import-DscResource -ModuleName PSDesiredStateConfiguration # 標準リソースモジュール

    # Windows Feature (IIS) をインストール
    Node $NodeName
    {
        WindowsFeature WebServer
        {
            Ensure = "Present"
            Name   = "Web-Server"
        }

        # Spooler サービスが実行状態であることを保証
        Service SpoolerService
        {
            Ensure  = "Running"
            Name    = "Spooler"
            StartupType = "Automatic"
        }

        # C:\temp ディレクトリの存在を保証
        File TempDirectory
        {
            Ensure          = "Present"
            Type            = "Directory"
            DestinationPath = "C:\temp"
        }
    }
}

並列プッシュ実装とエラーハンドリング

このスクリプトは、複数のターゲットノードに対してDSC構成を並列でプッシュし、各ノードの適用結果をログに出力します。タイムアウトと再試行ロジックも組み込んでいます。

# コード例1: 大規模DSCプッシュ適用スクリプト (Deploy-DscConfigurationParallel.ps1)

# スクリプト全体のトランスクリプトログを開始
$LogPath = Join-Path $PSScriptRoot "DscDeployment_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
Start-Transcript -Path $LogPath -Append -Force

# --- 設定パラメータ ---
# ターゲットノードリスト (FQDNまたはIPアドレス)
$TargetNodes = @("targethost01.example.com", "targethost02.example.com", "targethost03.example.com")
# $TargetNodes = @("localhost") # テスト用

# DSC構成ファイル名 (MOFファイル名と同じになるよう調整)
$DscConfigurationName = "ExampleDscConfig"
# DSC構成スクリプトのパス
$DscConfigurationScriptPath = Join-Path $PSScriptRoot "ExampleDscConfig.ps1"
# MOFファイルの出力ディレクトリ
$ConfigurationDataPath = Join-Path $PSScriptRoot "Configurations"
# 並列実行する最大ジョブ数 (RunspacePoolの容量に相当)
$MaxConcurrentJobs = 5
# 各DSC構成適用処理のタイムアウト (秒)
$DscApplyTimeoutSeconds = 300 # 5分
# 失敗時の再試行回数
$MaxRetries = 2
# 再試行間の待機時間 (秒)
$RetryDelaySeconds = 10

# エラー発生時の動作を停止に変更 (try/catchで捕捉しやすくするため)
$ErrorActionPreference = "Stop"

# --- 前処理 ---
# MOFファイルの生成ディレクトリが存在しない場合は作成
if (-not (Test-Path $ConfigurationDataPath)) {
    New-Item -Path $ConfigurationDataPath -ItemType Directory | Out-Null
}

Write-Host "--- DSC構成のコンパイル ---" -ForegroundColor Cyan
try {
    # DSC構成スクリプトをドットソースしてコンパイル関数を定義
    . $DscConfigurationScriptPath

    # MOFファイルを生成
    # ターゲットノードの数に関わらず、単一の構成MOFを生成 (NodeNameパラメータを利用)
    $Script:Global:$DscConfigurationName -NodeName $TargetNodes -OutputDirectory $ConfigurationDataPath -ErrorAction Stop
    Write-Host "MOFファイルが '$ConfigurationDataPath' に生成されました。" -ForegroundColor Green
}
catch {
    Write-Error "DSC構成のコンパイル中にエラーが発生しました: $($_.Exception.Message)"
    Exit 1 # スクリプトを終了
}

# --- 並列DSC適用処理 ---
Write-Host "--- 並列DSC構成適用開始 ($($TargetNodes.Count) ノード) ---" -ForegroundColor Cyan
$jobs = @()
$nodesToProcess = [System.Collections.Generic.Queue[string]]::new($TargetNodes)
$results = [System.Collections.Generic.List[PSCustomObject]]::new()
$startTime = Get-Date

while ($nodesToProcess.Count -gt 0 -or $jobs.Count -gt 0) {
    # 新しいジョブを開始 (最大同時実行数まで)
    while ($nodesToProcess.Count -gt 0 -and $jobs.Count -lt $MaxConcurrentJobs) {
        $node = $nodesToProcess.Dequeue()
        Write-Host "ノード '$node' へのDSC構成適用ジョブを開始します..."

        $jobScriptBlock = {
            param($NodeName, $ConfigPath, $ConfigName, $TimeoutSeconds, $MaxRetries, $RetryDelay)

            # このジョブスコープ内でのエラーアクション設定
            $ErrorActionPreference = "Stop"

            $structuredLog = [PSCustomObject]@{
                Timestamp = Get-Date
                Node = $NodeName
                Status = "InProgress"
                Message = "DSC configuration started."
                DurationSeconds = 0
                Error = $null
                Attempt = 0
            }

            for ($attempt = 1; $attempt -le ($MaxRetries + 1); $attempt++) {
                $structuredLog.Attempt = $attempt
                $structuredLog.Status = "Attempting"
                $structuredLog.Message = "Attempt $attempt of DSC configuration for $NodeName."
                Write-Output ($structuredLog | ConvertTo-Json -Compress) # 構造化ログを出力 (Receive-Jobで後で解析)

                $sw = [System.Diagnostics.Stopwatch]::StartNew()
                try {
                    # Start-DscConfiguration は時間がかかる可能性があるので、-Wait を指定しタイムアウトを考慮する
                    Start-DscConfiguration -Path $ConfigPath -ComputerName $NodeName -Force -Wait -Verbose -ErrorAction Stop
                    $sw.Stop()
                    $structuredLog.Status = "Succeeded"
                    $structuredLog.Message = "DSC configuration successfully applied."
                    $structuredLog.DurationSeconds = $sw.Elapsed.TotalSeconds
                    Write-Output ($structuredLog | ConvertTo-Json -Compress)
                    return $structuredLog # 成功したらループを抜ける
                }
                catch {
                    $sw.Stop()
                    $errorMessage = $_.Exception.Message
                    $structuredLog.Status = "Failed"
                    $structuredLog.Message = "DSC configuration failed on attempt $attempt: $errorMessage"
                    $structuredLog.Error = $_.Exception.ToString()
                    $structuredLog.DurationSeconds = $sw.Elapsed.TotalSeconds
                    Write-Output ($structuredLog | ConvertTo-Json -Compress)

                    if ($attempt -le $MaxRetries) {
                        Write-Output "Retrying DSC configuration for $NodeName in $RetryDelay seconds..."
                        Start-Sleep -Seconds $RetryDelay
                    } else {
                        Write-Error "Max retries ($MaxRetries) exceeded for $NodeName. Giving up."
                        return $structuredLog # 最終的に失敗
                    }
                }
            }
            return $structuredLog # ここには到達しないはずだが、念のため
        }

        # ジョブを投入。必要な変数を$usingスコープで渡す
        $newJob = Start-Job -ScriptBlock $jobScriptBlock -ArgumentList $node, $ConfigurationDataPath, $DscConfigurationName, $DscApplyTimeoutSeconds, $MaxRetries, $RetryDelaySeconds -Name "DSC_Deploy_$node"
        $jobs += $newJob
    }

    # 完了したジョブをチェックし、結果を収集
    $completedJobs = $jobs | Where-Object { $_.State -ne 'Running' -and $_.State -ne 'NotStarted' }

    foreach ($job in $completedJobs) {
        Write-Host "ジョブ $($job.Name) が完了しました (State: $($job.State))."

        # Receive-Jobでジョブの結果 (structuredLog) を取得
        $jobOutput = Receive-Job -Job $job -Keep # Keepで後で再取得可能に

        # 最後の構造化ログエントリが最終結果
        $finalLogEntry = $jobOutput | Where-Object { $_ -is [string] -and $_.StartsWith('{') } | ConvertFrom-Json | Select-Object -Last 1

        if ($null -ne $finalLogEntry) {
            $results.Add($finalLogEntry)
            if ($finalLogEntry.Status -eq "Succeeded") {
                Write-Host "ノード $($finalLogEntry.Node) のDSC構成適用が成功しました。" -ForegroundColor Green
            } else {
                Write-Host "ノード $($finalLogEntry.Node) のDSC構成適用が失敗しました: $($finalLogEntry.Message)" -ForegroundColor Red
            }
        } else {
            # 構造化ログがなかった場合のフォールバック
            $results.Add([PSCustomObject]@{
                Timestamp = Get-Date
                Node = $job.Name.Replace("DSC_Deploy_", "")
                Status = "Failed"
                Message = "No structured log output from job. Job state: $($job.State)."
                Error = ($job.ChildJobs | Select-Object -ExpandProperty Error -ErrorAction SilentlyContinue | Out-String)
                DurationSeconds = (Get-Date).Subtract($job.StartTime).TotalSeconds
                Attempt = 1
            })
            Write-Host "ノード $($job.Name.Replace("DSC_Deploy_", "")) からの結果を処理中にエラーが発生しました。" -ForegroundColor Red
        }

        # 完了したジョブをリストから削除
        $jobs = $jobs | Where-Object { $_.Id -ne $job.Id }
        Remove-Job -Job $job -Force | Out-Null # ジョブオブジェクトをクリーンアップ
    }

    # 処理中のジョブがない場合、ループを抜ける前に少し待機
    if ($nodesToProcess.Count -gt 0 -or $jobs.Count -gt 0) {
        Start-Sleep -Seconds 5 # ポーリング間隔
    }
}

$endTime = Get-Date
$totalDuration = ($endTime - $startTime).TotalSeconds
Write-Host "--- 全ノードへのDSC構成適用が完了しました ---" -ForegroundColor Cyan
Write-Host "合計処理時間: $($totalDuration) 秒" -ForegroundColor Cyan

# 結果の集計と表示
$results | Format-Table -AutoSize
$results | Export-Csv (Join-Path $PSScriptRoot "DscDeployment_Summary_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv") -NoTypeInformation

# スクリプト全体のトランスクリプトログを終了
Stop-Transcript

解説: * Start-Transcript でスクリプト全体の実行ログを記録。 * Configuration ブロックを実行してMOFファイルを生成。NodeName パラメータを使うことで、複数のノードに対して単一のMOFファイルを生成し、Start-DscConfiguration -ComputerName で各ノードにプッシュします。 * Start-Job を利用して各ターゲットノードへの Start-DscConfiguration 呼び出しを並列化。 * $MaxConcurrentJobs で同時に実行するジョブ数を制御し、リソースの枯渇を防ぎます。 * ジョブ内では try/catch を使用し、DSC適用時のエラーを捕捉。 * $using スコープを使って、メインスクリプトの変数をジョブスクリプトブロックに渡します。 * Wait-Job を使わずに、While ループ内で$_state -ne 'Running' を確認し、完了したジョブをポーリングして処理します。これにより、キューイングと並列実行をより柔軟に管理できます。 * 各ジョブの結果は PSCustomObject としてJSON形式で出力され、Receive-Job で取得後に解析されます。これにより、構造化されたログデータを取得できます。 * 再試行ロジックを各ジョブスクリプトブロック内に実装し、一時的なネットワーク障害などに対応。

検証(性能・正しさ)と計測スクリプト

性能計測

Measure-Command を使用して、並列化の効果と処理時間を計測します。多数のノードに対するDSC適用は、その特性上、ネットワークI/OやCPUリソースの競合が発生しやすいため、適切な並列度を見極めることが重要です。

例えば、上記のDeploy-DscConfigurationParallel.ps1スクリプト全体をMeasure-Commandで囲むことで、複数ノードに対するDSC適用の総時間を計測できます。

# コード例2: DSC適用性能計測と正しさの検証

# このスクリプトは、コード例1のDeploy-DscConfigurationParallel.ps1を呼び出して計測する例です。
# Deploy-DscConfigurationParallel.ps1 が存在し、テスト用の $TargetNodes が適切に設定されている前提です。

# --- 性能計測 ---
Write-Host "--- DSC構成適用性能計測 ---" -ForegroundColor Yellow

# 計測前に、DSC構成スクリプトのパスやターゲットノードリストを必要に応じて調整してください。
# 例: 仮想的なターゲットノードリストを作成
# $nodes = 1..10 | ForEach-Object { "localhost" } # ローカルホストで10回実行するシミュレーション
# (Get-Content ".\Deploy-DscConfigurationParallel.ps1").Replace('targethost01.example.com"', '"localhost"') | Set-Content ".\temp_Deploy-DscConfigurationParallel.ps1"

$measurement = Measure-Command {
    # ここでDeploy-DscConfigurationParallel.ps1を実行
    # 必要に応じて、パスやターゲットノードリストをオーバーライドする引数を渡す
    & ".\Deploy-DscConfigurationParallel.ps1"
}

Write-Host "`nDSC構成適用全体の処理時間: $($measurement.TotalSeconds) 秒" -ForegroundColor Yellow

# --- 正しさの検証 ---
Write-Host "`n--- DSC構成の正しさ検証 (Test-DscConfiguration) ---" -ForegroundColor Yellow

$NodesToVerify = @("targethost01.example.com") # 検証したいノードを指定 (例として1つ)
# $NodesToVerify = @("localhost") # テスト用

foreach ($node in $NodesToVerify) {
    Write-Host "ノード '$node' でDSC構成の正しさを検証しています..."
    try {
        # Test-DscConfiguration を実行して現在の状態が望ましい状態と一致するかを確認
        # Pull Server を利用している場合は、Pull Server との通信が必要になる場合があります。
        # Push 方式の場合、ローカルにMOFファイルが適用されているため、それとの比較になります。
        $isConsistent = Test-DscConfiguration -ComputerName $node -ErrorAction Stop

        if ($isConsistent) {
            Write-Host "ノード '$node': 構成は望ましい状態と一致しています。(OK)" -ForegroundColor Green
        } else {
            Write-Host "ノード '$node': 構成は望ましい状態と一致していません。(NG)" -ForegroundColor Red
            # 詳細な不一致項目を知りたい場合は Get-DscConfiguration を利用
            # (Get-DscConfiguration -ComputerName $node).Result | Format-List
        }
    }
    catch {
        Write-Error "ノード '$node' でのDSC構成検証中にエラーが発生しました: $($_.Exception.Message)"
    }
}

# --- ShouldContinue の利用例 ---
function Invoke-CriticalDscDeployment {
    param(
        [string]$NodeName
    )
    if ($PSCmdlet.ShouldContinue("確認", "本当にノード '$NodeName' にこのクリティカルなDSC構成を適用しますか?")) {
        Write-Host "ノード '$NodeName' へのクリティカルなDSC構成適用を実行します..."
        # Start-DscConfiguration -ComputerName $NodeName ...
    } else {
        Write-Warning "ノード '$NodeName' へのDSC構成適用はキャンセルされました。"
    }
}

# 例: 重要な構成変更を行う前にユーザー確認を求める
# Invoke-CriticalDscDeployment -NodeName "critical-server.example.com"

正しさの検証

Test-DscConfiguration コマンドレットを使用することで、ターゲットノードの現在の状態がDSC構成によって定義された望ましい状態と一致しているかを確認できます。これにより、適用された構成が意図通りに機能しているかを検証します。

運用:ログローテーション/失敗時再実行/権限

ロギング戦略

  • トランスクリプトログ: スクリプト全体の実行履歴を時系列で記録するために Start-TranscriptStop-Transcript を利用します。これは、スクリプトのフローや発生したエラーを詳細に追跡するのに役立ちます。ファイルサイズが肥大化するのを防ぐため、定期的なローテーション(古いログの削除やアーカイブ)が必要です。
  • 構造化ログ: 各ノードへのDSC適用結果は、PSCustomObject で構造化し、ConvertTo-Json を使って出力します。これにより、Status (Succeeded/Failed)、Node、Duration、ErrorMessage などの情報をプログラムで容易に解析できるようになり、ダッシュボードでの可視化やアラートシステムとの連携が容易になります。これらの構造化ログは、別途ファイルに追記するか、SplunkやLog Analyticsなどの集中ログ管理システムに転送することを検討します。

失敗時再実行

上記コード例では、MaxRetries パラメータによって、DSC適用が失敗した場合の再試行ロジックを実装しています。一時的なネットワークの問題やリソースの競合などによる瞬時的な失敗に対応できます。恒久的な失敗(例: リソースが常に存在しない)に対しては、アラートを上げて手動介入を促す必要があります。また、スクリプトの実行が全体として失敗した場合に、失敗したノードリストのみを抽出し、再実行する別のスクリプトを作成することも有効です。

権限

  • DSCプッシュ元: DSC構成をターゲットノードにプッシュするには、WinRMを介してリモートで Start-DscConfiguration を実行する権限が必要です。通常は、ターゲットノードのローカル管理者グループに属するアカウント、またはドメイン管理者アカウントを使用します。
  • DSC適用先: ターゲットノード上でDSC構成を適用するプロセスは、通常 Local System アカウントで実行されます。これにより、サービスインストールやレジストリ変更などの特権操作が可能になります。ただし、DSCリソースによっては、特定のクレデンシャルを要求する場合があります。その際は PsDscRunAsCredential プロパティを活用します。

落とし穴

PowerShell 5.1 vs 7.xの差

  • ForEach-Object -Parallel: PowerShell 7.0以降で導入された強力な並列処理機能です。本稿では Start-Job を利用していますが、PowerShell 7.x 環境であれば ForEach-Object -Parallel の利用も検討できます。ただし、$using スコープの動作やエラーハンドリングの仕方に違いがあるため注意が必要です。
  • DSCリソースの互換性: PowerShell 5.1のWindows Management Framework (WMF) 5.1に含まれるDSCリソースと、PowerShell 7.xで提供されるDSCリソースには互換性の問題が生じる場合があります。特に、コミュニティ製のDSCリソースを使用する場合は、ターゲットノードのPowerShellバージョンに対応しているか確認が必要です。
  • デフォルトエンコーディング: PowerShell 5.1と7.xでは、デフォルトのファイルエンコーディングが異なります(PS5.1はANSI/Shift-JIS系、PS7.xはUTF-8)。スクリプトやログの入出力で文字化けが発生しないよう、明示的に -Encoding UTF8 などを指定することが推奨されます。

スレッド安全性と共有変数

Start-JobForEach-Object -Parallel のような並列処理では、共有変数へのアクセスに注意が必要です。メインスクリプトで定義した変数をジョブ内で参照する場合、$using: スコープを使用しますが、これは変数のコピーが渡されるだけで、共有ではありません。ジョブ間でデータを共有する必要がある場合は、ファイルやデータベース、またはミューテックスを使用した同期機構を検討する必要があります。本稿の例では、各ジョブが独立してログオブジェクトを作成し、メインスクリプトで後で集計するため、スレッド安全性の問題は最小限に抑えられています。

UTF-8問題

前述の通り、スクリプトファイル自体やログファイルのエンコーディングは、文字化けを防ぐためにUTF-8(BOMなしが好ましいが、環境によってはBOMありが安全)に統一することが重要です。Set-Content -Encoding UTF8Export-Csv -Encoding UTF8 を積極的に利用しましょう。

安全対策

Just Enough Administration (JEA) / JIT

DSCのプッシュ操作は強力な権限を必要としますが、全ての管理者アカウントに無制限の権限を与えるのはセキュリティリスクです。Just Enough Administration (JEA) を導入することで、DSCプッシュ操作に必要な最小限のコマンドレットのみを実行できるエンドポイントを構成し、特権の過剰付与を防ぐことができます。また、Just-In-Time (JIT) アクセス管理と組み合わせることで、必要な時のみ一時的に昇格した権限を付与し、作業が完了したら自動的に権限を剥奪するといった運用も可能です。

機密の安全な取り回し / SecretManagement

DSC構成にパスワードやAPIキーなどの機密情報を含める場合、プレーンテキストでの保存は厳禁です。PowerShellの SecretManagement モジュールを利用することで、Windows Credential Manager や Azure Key Vault などと連携し、機密情報を安全に保存・取得できます。DSC構成内で Get-Secret を利用し、実行時に機密情報を取得する仕組みを構築することを検討してください。

まとめ

DSCは、サーバー構成管理を自動化し、一貫性を保つための不可欠なツールです。本記事では、大規模環境でのDSCプッシュ運用に焦点を当て、PowerShellのジョブ機能を用いた並列処理、エラーハンドリング、タイムアウト、再試行ロジックの実装方法を具体的に示しました。

運用においては、可観測性を高めるための詳細なロギング戦略(トランスクリプトログと構造化ログの併用)、性能ボトルネックの特定と最適化(Measure-Command)、そしてセキュリティ(JEA、SecretManagement)への配慮が極めて重要です。これらのプラクティスを組み合わせることで、堅牢で効率的なDSC運用を実現し、システム全体の信頼性と安定性を向上させることができます。DSCを最大限に活用し、日々のWindows運用をよりプロアクティブなものへと進化させましょう。

ライセンス:本記事のテキスト/コードは特記なき限り CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。

コメント

タイトルとURLをコピーしました