Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)

Azure runbook 예제 - 6시간 동안 수행 중인 VM을 중지

지난 글의 runbook 스크립트와 그 제어에 이어서,

Azure runbook을 PowerShell 또는 C# 코드로 실행하는 방법
; https://www.sysnet.pe.kr/2/0/11516

보통 runbook에서 어떤 동작을 구현하고 싶다면 Azure Portal의 Cloud Shell을 통해 Azure 명령어를 실행해 보고 나서 옮기는 것이 편합니다. 가령 Azure의 VM 하나를 멈추게 만들고 싶은 경우 아래와 같이 테스트해 볼 수 있습니다.

PS Azure:\> Stop-AzureRmVM -Name "vm1" -ResourceGroupName "myservice2" -Force

명령어 수행이 잘 되는 것을 확인했으면 이제 적절하게 인자를 골라내 다음과 같이 runbook 스크립트를 만듭니다.

# runbook - stop_vm

param (
    [Parameter(Mandatory=$true)][object]$targetVM
)

Stop-AzureRmVM -Name $targetVM.VMName -ResourceGroupName "myservice2" -Force

그런데, 위의 스크립트는 다음과 같은 오류가 발생합니다.

Stop-AzureRmVM : Run Login-AzureRmAccount to login.
At line:7 char:1
+ Stop-AzureRmVM -Name $targetVM.VMName -ResourceGroupName "myservice2" -Force
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [Stop-AzureRmVM], PSInvalidOperationException
    + FullyQualifiedErrorId : InvalidOperation,Microsoft.Azure.Commands.Compute.StopAzureVMCommand

왜냐하면 Azure runbook 실행 환경 내에서 Stop-AzureRmVM과 같은 Azure Automation 명령어를 사용하려면 실행 문맥에서 인증을 필요로 하기 때문입니다. 의외로 이 방법을 알아내는 것이 좀 힘들었는데요, Automation Accounts를 생성하는 경우 기본 생성된 runbook 중에 "AzureAutomationTutorial"을 통해 알아낼 수 있었습니다. 그래서 다음과 같이 작성하면 됩니다.

param (
    [Parameter(Mandatory=$true)][object]$targetVM
)

$azureConnection = Get-AutomationConnection -Name AzureRunAsConnection

Add-AzureRmAccount -ServicePrincipal -Tenant $azureConnection.TenantID -ApplicationID $azureConnection.ApplicationID -CertificateThumbprint $azureConnection.CertificateThumbprint
    

Stop-AzureRmVM -Name $targetVM.VMName -ResourceGroupName "myservice2" -Force

특이하게 Get-AutomationConnection 명령어의 경우 검색해 봐도 정식 도움말이 (2018-05-09 기준) 없습니다. 아마도 Azure runbook 실행 환경 내에서만 특별하게 제공되는 듯한데 이 때문에 "AzureAutomationTutorial" 예제 스크립트 외에는 달리 참조할만한 곳이 없었습니다.

이렇게 만들어진 runbook을 PowerShell 클라이언트로 다음과 같이 실행할 수 있습니다.

PS C:\WINDOWS\system32> $json = '{ "VMName": "vm1" }' | ConvertFrom-Json

PS C:\WINDOWS\system32> $arg = @{ 'targetVM' = $json }

PS C:\WINDOWS\system32> $RBParams = @{ AutomationAccountName = 'testrunbook'; ResourceGroupName = 'myservice2'; Name = 'stop_vm'; Parameters = $arg }

PS C:\WINDOWS\system32> Start-AzureRmAutomationRunbook @RBParams

C#으로 실행하는 방법은 지난번 글과 다르지 않으며 첨부 파일은 이 글의 예제에 맞게 수정한 것입니다.




이번엔 약간 다른 시나리오를 생각해 보겠습니다. 예를 들어, VM이 6시간 이상 실행 중일 때 중지하도록 만들고 싶다면 어떻게 해야 할까요? 그러려면 우선 VM의 운영 시간을 구해야 하는데 다음의 글에 따라,

Get-AzureRmVM, Our script, Extra credit : Obtain Azure VM uptime
; https://4sysops.com/archives/check-azure-vm-status-with-powershell/

Get-AzureRmVM 명령어를 이용해 알아낼 수 있습니다.

PS C:\WINDOWS\system32> $vmStatus = Get-AzureRmVM -ResourceGroupName "myservice2" -Name "vm1" -Status
WARNING: Get-AzureRmVM: A property of the output of this cmdlet will change in an upcoming breaking change release. The Storag
eAccountType property for a DataDisk will return Standard_LRS and Premium_LRS

PS C:\WINDOWS\system32> $vmStatus

ResourceGroupName    : myservice2
Name                 : vm1
Disks[0]             : 
  Name               : vm1_OsDisk_1_eab77339733f42a29dd77430fccc8a42
  Statuses[0]        : 
    Code             : ProvisioningState/succeeded
    Level            : Info
    DisplayStatus    : Provisioning succeeded
    Time             : 2018-05-09 오전 6:29:53
PlatformFaultDomain  : 0
PlatformUpdateDomain : 0
Statuses[0]          : 
  Code               : ProvisioningState/succeeded
  Level              : Info
  DisplayStatus      : Provisioning succeeded
  Time               : 2018-05-09 오전 6:29:53
Statuses[1]          : 
  Code               : PowerState/deallocated
  Level              : Info
  DisplayStatus      : VM deallocated


PS C:\WINDOWS\system32> $vmStatus.Statuses[1].Code
PowerState/deallocated

현재 위의 상태는 "중지됨(할당 취소됨)"입니다. 다음은 각각의 경우에 대한 Statuses 값을 보여줍니다.

[시작 중]

ResourceGroupName    : myservice2
Name                 : vm1
Disks[0]             : 
  Name               : vm1_OsDisk_1_eab77339733f42a29dd77430fccc8a42
  Statuses[0]        : 
    Code             : ProvisioningState/succeeded
    Level            : Info
    DisplayStatus    : Provisioning succeeded
    Time             : 2018-05-09 오전 7:34:34
PlatformFaultDomain  : 0
PlatformUpdateDomain : 0
Statuses[0]          : 
  Code               : ProvisioningState/updating
  Level              : Info
  DisplayStatus      : Updating
Statuses[1]          : 
  Code               : PowerState/starting
  Level              : Info
  DisplayStatus      : VM starting

[실행 중]

Name                 : vm1
Disks[0]             : 
  Name               : vm1_OsDisk_1_eab77339733f42a29dd77430fccc8a42
  Statuses[0]        : 
    Code             : ProvisioningState/succeeded
    Level            : Info
    DisplayStatus    : Provisioning succeeded
    Time             : 2018-05-09 오전 7:34:34
PlatformFaultDomain  : 0
PlatformUpdateDomain : 0
VMAgent              : 
  VmAgentVersion     : Unknown
  Statuses[0]        : 
    Code             : ProvisioningState/Unavailable
    Level            : Warning
    DisplayStatus    : Not Ready
    Message          : VM status blob is found but not yet populated.
    Time             : 2018-05-09 오전 7:35:39
Statuses[0]          : 
  Code               : ProvisioningState/succeeded
  Level              : Info
  DisplayStatus      : Provisioning succeeded
  Time               : 2018-05-09 오전 7:35:31
Statuses[1]          : 
  Code               : PowerState/running
  Level              : Info
  DisplayStatus      : VM running

[할당을 취소하는 중]

ResourceGroupName    : myservice2
Name                 : vm1
Disks[0]             : 
  Name               : vm1_OsDisk_1_eab77339733f42a29dd77430fccc8a42
  Statuses[0]        : 
    Code             : ProvisioningState/succeeded
    Level            : Info
    DisplayStatus    : Provisioning succeeded
    Time             : 2018-05-09 오전 7:34:34
PlatformFaultDomain  : 0
PlatformUpdateDomain : 0
VMAgent              : 
  VmAgentVersion     : 2.7.41491.875
  Statuses[0]        : 
    Code             : ProvisioningState/succeeded
    Level            : Info
    DisplayStatus    : Ready
    Message          : GuestAgent is running and accepting new configurations.
    Time             : 2018-05-09 오전 7:41:29
Statuses[0]          : 
  Code               : ProvisioningState/updating
  Level              : Info
  DisplayStatus      : Updating
Statuses[1]          : 
  Code               : PowerState/deallocating
  Level              : Info
  DisplayStatus      : VM deallocating

따라서, Statuses[1].DisplayStatus == "VM running" 상태를 기준으로 Statuses[0].Time을 현재 시간과 비교해 6시간이 지난 경우 VM을 중지하라고 스크립트를 구성하면 됩니다. 다음은 이를 반영해 stop_vm 스크립트를 작성한 것입니다.

param ([Parameter(Mandatory=$true)][object]$targetVM)

$azureConnection = Get-AutomationConnection -Name AzureRunAsConnection

Add-AzureRmAccount -ServicePrincipal -Tenant $azureConnection.TenantID `
    -ApplicationID $azureConnection.ApplicationID -CertificateThumbprint $azureConnection.CertificateThumbprint

$vmStatus = Get-AzureRmVM -ResourceGroupName "myservice2" -Name $targetVM.VMName -Status

if ($vmStatus.Statuses[1].DisplayStatus -eq "VM running")
{
    Write-Output "VM: Running"

    $uptime = New-TimeSpan -Start $vmStatus.Statuses[0].Time -End (Get-Date)
    if ($uptime.TotalHours -gt 6)
    {
        Write-Output "VM update: {$uptime.TotalHours}"
        Stop-AzureRmVM -Name $targetVM.VMName -ResourceGroupName "myservice2" -Force
    }
    else
    {
        Write-Output "VM update: {$uptime.TotalHours}"
    }
}
else
{
    Write-Output "VM: Not running"
}




6시간마다 중지하도록 stop_vm runbook도 완성했는데, 이 스크립트를 사용자가 일일이 시간마다 실행해 줄 수는 없습니다. ^^ 당연히 자동화해야 할 텐데요, 이를 위해 runbook에서는 다음과 같이 "schedule(일정)" 기능을 제공합니다.

stop_vm_runbook_1.png

특정 시간에 한 번만 실행하는 것도 가능하지만, 이 글의 시나리오를 위해선 되풀이하도록 설정하는 것이 현실적입니다. 따라서 다음과 같이 "1시간 간격"으로 되풀이하도록 일정을 만들 수 있습니다.

stop_vm_runbook_2.png

현재 Azure에서 설정 가능한 되풀이 단위는 "시간, 일, 주, 월"이며 따라서 "1 시간"이 최소 간격입니다. 만약 다른 시간 간격을 사용하고 싶다면 Webhook으로 노출시켜 원하는 간격으로 직접 호출하는 스케줄링을 하면 됩니다. 시간 간격 후에는, 이 글에서 실습하는 stop_vm runbook은 매개 변수도 필요하기 때문에 다음과 같이 설정해 주어야 합니다. (참고로 아래와 같이 Azure Portal UI를 통해 json 유형의 값으로 매개 변수를 채우면 schedule로 인한 호출 시 오류가 발생합니다. 이유는 나중에 설명합니다.)

stop_vm_runbook_3.png

이후부터는, 설정된 시간 간격과 매개 변수로 runbook이 자동으로 실행됩니다.

당연히 이런 일정 등록 작업도 PowerShell이나 C#으로 가능합니다. 그런데, 위에서처럼 runbook에 schedule을 할당하는 방법은 없고 "Automation Account" 계정 수준에서 schedule을 만든 다음,

# Automation Account 내에 schedule 생성

$StartTime = Get-Date "10:25:00"
New-AzureRmAutomationSchedule -AutomationAccountName "testrunbook" -Name "Schedule01" -StartTime $StartTime -HourInterval 1  -ResourceGroupName "myservice2"

이것을 개별 runbook에 연결하는 방법으로 등록할 수 있도록 하고 있습니다.

# Automation Account 내에 생성해 두었던 schedule과 runbook을 연결

$vmParams = '{ "VMName": "vm1" }' | ConvertFrom-Json
$runbookParams = @{"targetVM"=$vmParams}
Register-AzureRmAutomationScheduledRunbook -AutomationAccountName "testrunbook" -RunbookName "stop_vm" -ScheduleName "Schedule01" -ResourceGroupName "myservice2" -Parameters $runbookParams

어찌 보면 schedule을 재사용할 수 있도록 해주므로 좋은 방법이긴 합니다. (아마도 runbook 단위로 생성하는 방법도 있겠지만, 제가 방법을 못 찾은 걸 수도 있습니다.)

참고로, Automation Account 단위로 재사용할 수 있는 변수도 추가할 수 있는데 다음과 같이 생성합니다.

$vmParams = '{ "VMName": "vm1" }' | ConvertFrom-Json
New-AzureRmAutomationVariable -AutomationAccountName "testrunbook" -Name "targetVM" -Encrypted $False -Value $vmParams -ResourceGroupName "myservice2"




이 외에도, VM의 성능 메트릭(Metrics)에 따라 WebHook이나 내부의 runbook을 호출하도록 설정하는 것이 가능합니다. 이에 대해서는 다음의 글에서 설명하고 있는데, 지금까지의 내용을 이해하셨다면 한번 훑어보기만 해도 금방 이해가 되실 것입니다.

Using Azure Automation to take action on Azure Alerts
; https://azure.microsoft.com/en-us/blog/using-azure-automation-to-take-actions-on-azure-alerts/




Stop-AzureRmVM 명령어는 기본적으로 명령어 실행 모드가 동기 방식입니다. 따라서, 대상 VM이 Azure에서 중지될 때까지 기다리게 되는데 이 시간이 꽤 깁니다. 따라서 여러 개의 VM을 중지하는 경우에는 비동기로 실행하는 것을 권장하며, 이를 위해 명령어에 -AsJob 옵션을 주면 됩니다.

Stop-AzureRmVM -Name "vm1" -ResourceGroupName "myservice2" -Force -AsJob

그런데 문제는, Azure runbook 실행 환경에서는 Stop-AzureRmVM에 대해 AsJob 옵션을 허용하지 않아 다음과 같은 오류가 발생합니다.

Stop-AzureRmVM : A parameter cannot be found that matches parameter name 'AsJob'.
At line:12 char:16
+ Stop-AzureRmVM -AsJob -Name "vm1" -ResourceGroupName ...
+                ~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [Stop-AzureRmVM], ParameterBindingException
    + FullyQualifiedErrorId : NamedParameterNotFound,Microsoft.Azure.Commands.Compute.StopAzureVMCommand

그래서, 만약 여러 개의 VM을 제어하고 싶다면 runbook 스크립트 내에서 for-each를 통해 VM을 제어해서는 안 되고 runbook을 실행하는 측에서 해당 runbook을 제어해야 할 VM의 이름을 넘겨, 이를 여러 번 호출하는 방식으로 구현해야 합니다.




일정 등록을 할 때, 다음과 같은 오류가 발생할 수 있습니다.

PS C:\WINDOWS\system32> New-AzureRmAutomationSchedule -AutomationAccountName "testrunbook" -Name "Schedule01" -StartTime $StartTime -HourInterval 1  -ResourceGroupName "myservice2"
New-AzureRmAutomationSchedule : BadRequest: Argument requestScheduleData with value Orchestrator.Schedules.DataAccess.Models.ScheduleAllData is not valid. Error message: The start time of the schedule must be at least 5 minutes after the time you create the schedule. 
At line:1 char:1
+ New-AzureRmAutomationSchedule -AutomationAccountName "testrunbook" -N ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : CloseError: (:) [New-AzureRmAutomationSchedule], CloudException
    + FullyQualifiedErrorId : Microsoft.Azure.Commands.Automation.Cmdlet.NewAzureAutomationSchedule

말 그대로, -StartTime의 인자로 전달한 시간이 현재 시간으로부터 5분 이내로 설정된 경우이기 때문입니다. 따라서, 다음과 같은 식으로 생성하면 안전하게 등록할 수 있습니다. (참고로, region에 따라 시간이 다릅니다. 제가 테스트 한 바로는 15분인 경우도 있었습니다.)

$StartTime = Get-Date
$StartTime = $StartTime.AddMinutes(6)
New-AzureRmAutomationSchedule -AutomationAccountName "testrunbook" -Name "Schedule01" -StartTime $StartTime -HourInterval 1  -ResourceGroupName "myservice2"




이 글을 실습하는 중에 다음과 같은 runbook 실행 오류 로그가 남을 수 있습니다.

Get-AzureRmVM : Cannot validate argument on parameter 'Name'. The argument is null or empty. Provide an argument that 
is not null or empty, and then try the command again.
At line:12 char:65
+ ... reRmVM -ResourceGroupName "myservice2" -Name $targetVM.VMName -Status
+                                                  ~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidData: (:) [Get-AzureRmVM], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationError,Microsoft.Azure.Commands.Compute.GetAzureVMCommand

말 그대로, runbook의 $targetVM에 전달하는 인자가 정상적으로 역직렬화가 안 된 것입니다. 이 부분이 좀 이상한데요. runbook에 등록하는 일정의 매개 변수를 다음과 같이 설정하면,

stop_vm_runbook_3.png

{ "VMName": "vm1" }

생성 후 다시 매개 변수를 확인했을 때 다음과 같이 출력됩니다.

{ \"VMName\": \"vm1\" }

재미있는 것은, 위와 같이 Azure Portal의 UI를 이용하지 않고 직접 프로그래밍을 통해 설정하는 경우에는 매개 변숫값이 겹따옴표에 대한 escape 문자 표현을 포함하고 있지 않다는 점입니다. 따라서 complex type에 대해 schedule의 매개 변수를 설정하려면 다음과 같이 코드로 설정해야 합니다.

# Automation Account 내에 생성해 두었던 schedule과 runbook을 연결

$vmParams = '{ "VMName": "vm1" }' | ConvertFrom-Json
$runbookParams = @{"targetVM"=$vmParams}
Register-AzureRmAutomationScheduledRunbook -AutomationAccountName "testrunbook" -RunbookName "stop_vm" -ScheduleName "Schedule01" -ResourceGroupName "myservice2" -Parameters $runbookParams

위와 같이 해서 scheduler를 생성하면 매개 변수를 확인했을 때 정상적으로 { "VMName": "vm1" }라고 나옵니다. 참고로, Azure portal UI 화면에서 다음과 같은 설정으로 입력했을 때 모두 실패했습니다.

{ 'VMName': 'vm1' }

{ ""VMName"": ""vm1"" }

{ VMName: vm1 }

{ "VMName": "vm1" }

아마도 azure의 입력 버그가 아닌가 싶은데... 두고 봐야겠습니다. ^^




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 5/10/2018]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2018-06-27 02시49분
이 글의 마지막 실습에서 { "VMName": "vm1" } 텍스트를 설정했는데도 오류가 나는 것은 해당 스크립트의 param 인자 타입을 다음과 같이 object에서 PSObject로 바꿔주면 해결됩니다.

param (
    [Parameter(Mandatory=$true)][PSObject]$user
)

관련해서 다음의 Form에서 질문해 답변을 들었습니다. ^^

How to pass complex typed argument in Azure runbook schedule's parameter?
; https://social.msdn.microsoft.com/Forums/azure/en-US/5d911ba5-6b7e-4182-9873-35c37b7012b7/how-to-pass-complex-typed-argument-in-azure-runbook-schedules-parameter?forum=azureautomation
정성태

... 31  32  33  34  35  36  37  38  [39]  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12650정성태5/17/20217941기타: 82. OpenTabletDriver의 버튼에 더블 클릭을 매핑 및 게임에서의 지원 방법
12649정성태5/16/20219287.NET Framework: 1059. 세대 별 GC(Garbage Collection) 방식에서 Card table의 사용 의미 [1]
12648정성태5/16/20217941사물인터넷: 66. PC -> FTDI -> NodeMCU v1 ESP8266 기기를 UART 핀을 연결해 직렬 통신하는 방법파일 다운로드1
12647정성태5/15/20219171.NET Framework: 1058. C# - C++과의 연동을 위한 구조체의 fixed 배열 필드 사용파일 다운로드1
12646정성태5/15/20218291사물인터넷: 65. C# - Arduino IDE의 Serial Monitor 기능 구현파일 다운로드1
12645정성태5/14/20218022사물인터넷: 64. NodeMCU v1 ESP8266 - LittleFS를 이용한 와이파이 접속 정보 업데이트파일 다운로드1
12644정성태5/14/20219164오류 유형: 719. 윈도우 - 제어판의 "프로그램 및 기능" / "Windows 기능 켜기/끄기" 오류 0x800736B3
12643정성태5/14/20218334오류 유형: 718. 서버 유형의 COM+ 사용 시 0x80080005(Server execution failed) 오류 발생
12642정성태5/14/20219255오류 유형: 717. The 'Microsoft.ACE.OLEDB.12.0' provider is not registered on the local machine.
12641정성태5/13/20218961디버깅 기술: 179. 윈도우용 .NET Core 3 이상에서 Windbg의 sos 사용법
12640정성태5/13/202111900오류 유형: 716. RDP 연결 - Because of a protocol error (code: 0x112f), the remote session will be disconnected. [1]
12639정성태5/12/20218792오류 유형: 715. Arduino: Open Serial Monitor - The module '...\detection.node' was compiled against a different Node.js version using NODE_MODULE_VERSION
12638정성태5/12/20219698사물인터넷: 63. NodeMCU v1 ESP8266 - 펌웨어 내 파일 시스템(SPIFFS, LittleFS) 및 EEPROM 활용
12637정성태5/10/20219359사물인터넷: 62. NodeMCU v1 ESP8266 보드의 A0 핀에 다중 아날로그 센서 연결 [1]
12636정성태5/10/20219523사물인터넷: 61. NodeMCU v1 ESP8266 보드의 A0 핀 사용법 - FSR-402 아날로그 압력 센서 연동파일 다운로드1
12635정성태5/9/20218852기타: 81. OpenTabletDriver를 (관리자 권한으로 실행하지 않고도) 관리자 권한의 프로그램에서 동작하게 만드는 방법
12634정성태5/9/20217936개발 환경 구성: 572. .NET에서의 신뢰도 등급 조정 - 외부 Manifest 파일을 두는 방법파일 다운로드1
12633정성태5/7/20219394개발 환경 구성: 571. UAC - 관리자 권한 없이 UIPI 제약을 없애는 방법
12632정성태5/7/20219561기타: 80. (WACOM도 지원하는) Tablet 공통 디바이스 드라이버 - OpenTabletDriver
12631정성태5/5/20219498사물인터넷: 60. ThingSpeak 사물인터넷 플랫폼에 ESP8266 NodeMCU v1 + 조도 센서 장비 연동파일 다운로드1
12630정성태5/5/20219820사물인터넷: 59. NodeMCU v1 ESP8266 보드의 A0 핀 사용법 - CdS Cell(GL3526) 조도 센서 연동파일 다운로드1
12629정성태5/5/202111559.NET Framework: 1057. C# - CoAP 서버 및 클라이언트 제작 (UDP 소켓 통신) [1]파일 다운로드1
12628정성태5/4/20219525Linux: 39. Eclipse 원격 디버깅 - Cannot run program "gdb": Launching failed
12627정성태5/4/202110236Linux: 38. 라즈베리 파이 제로 용 프로그램 개발을 위한 Eclipse C/C++ 윈도우 환경 설정
12626정성태5/3/202110242.NET Framework: 1056. C# - Thread.Suspend 호출 시 응용 프로그램 hang 현상 (2)파일 다운로드1
12625정성태5/3/20219203오류 유형: 714. error CS5001: Program does not contain a static 'Main' method suitable for an entry point
... 31  32  33  34  35  36  37  38  [39]  40  41  42  43  44  45  ...