Post

Azure DevOps pipeline Terraformal

Azure-ban erőforrást sok féle képpen létrehozhatunk, de bizonyos idő után biztos eljutunk az Infrastructure as code (IaC) megoldásokig. Ha már rendelkezésünkre áll a kellő tudás ahhoz, hogy kész infra kódokat írjunk, jöhetnek a pipeline megoldások, amikor is a központi tárból nem csak magát az alkalmazást hozhatjuk létre, de a hozzá tartozó infrastruktúrát is. Ebbe a postban lépésről lépésre végigmegyek egy Terraformos-os VM környezet létrehozásán az Azure DevOps-al.

Előfeltételek

Környezet létrehozásához a következőkre van szükség:

  • Service Principal (SP)
  • Azure DevOps regisztráció
  • Azure előfizetés amibe van pár $
  • Ingyenes DevOps esetén, ha még MSDN se áll rendelkezsére, lehet igényelni ingyenes Parallel job-ot (legalul leírom hogyan)

Storage Account Terraform számára

A Terraform-nak szüksége van egy helyre, ahova a state file-t mentheti, illetve a későbbiekben hozzá is férhet. Hozzunk létre egy storage account-ot, amit direkt erre a célra fogunk használni.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
az login

#Ha több előfizetéshez van jogosultsága a felhasználónknak

az account list

az account set --subscription "Előfizetés_neve"

#----------------------------------------------------------

$RESOURCE_GROUP_NAME="terraform_rg"
#egyedi névre van szükség régió szinten
$STORAGE_ACCOUNT_NAME="terraform_sa_3f37edh3"

# Resource Group létrehozása
az group create -l westeurope -n $RESOURCE_GROUP_NAME

# Storage Account létrehozása 
az storage account create -n $STORAGE_ACCOUNT_NAME -g $RESOURCE_GROUP_NAME -l westeurope --sku Standard_LRS

# Storage Account blob létrehozása
az storage container create  --name tfstate --account-name $STORAGE_ACCOUNT_NAME

Azure DevOps projekt létrehozása

Hozzunk létre egy üres projektet, amelyben eltároljuk a szükséges konfigurációs file-okat, illetve ahonnan tudjuk majd futtatni a pipeline-t. http://dev.azure.com

img-description
img-description

Ezzel el is készült az üres projekt, most pedig hozzuk létre egy service connection-t, amely segítségével erőforrásokat hozhatunk létre az előfizetésünkben.

Service connection létrehozása

Ezt már egy előző postban leírtam, hogy hogyan is kell létrehozni, mely itt található.

Terraform file létrehozása

A példa feladatban egy egyszerű VM-et fogunk létrehozni, amelyet a Microsoft Docs-ból másoltam ki. Eredeti verzió itt található.

Hozzuk létre a Terraform mappába a Terraform file-t.

img-description
img-description
img-description

Illesszük be a következőt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# Configure the Microsoft Azure Provider
provider "azurerm" {
    # The "feature" block is required for AzureRM provider 2.x.
    # If you're using version 1.x, the "features" block is not allowed.
    version = "~>2.0"
    features {}
}
 
terraform {
  backend "azurerm" {}
}

data "azurerm_client_config" "current" {}

# Create a resource group if it doesn't exist
resource "azurerm_resource_group" "myterraformgroup" {
    name     = "myResourceGroup2"
    location = "westeurope"

    tags = {
        environment = "Terraform Demo"
    }
}

# Create virtual network
resource "azurerm_virtual_network" "myterraformnetwork" {
    name                = "myVnet"
    address_space       = ["10.0.0.0/16"]
    location            = "eastus"
    resource_group_name = azurerm_resource_group.myterraformgroup.name

    tags = {
        environment = "Terraform Demo"
    }
}

# Create subnet
resource "azurerm_subnet" "myterraformsubnet" {
    name                 = "mySubnet"
    resource_group_name  = azurerm_resource_group.myterraformgroup.name
    virtual_network_name = azurerm_virtual_network.myterraformnetwork.name
    address_prefixes       = ["10.0.1.0/24"]
}

# Create public IPs
resource "azurerm_public_ip" "myterraformpublicip" {
    name                         = "myPublicIP"
    location                     = "eastus"
    resource_group_name          = azurerm_resource_group.myterraformgroup.name
    allocation_method            = "Dynamic"

    tags = {
        environment = "Terraform Demo"
    }
}

# Create Network Security Group and rule
resource "azurerm_network_security_group" "myterraformnsg" {
    name                = "myNetworkSecurityGroup"
    location            = "eastus"
    resource_group_name = azurerm_resource_group.myterraformgroup.name

    security_rule {
        name                       = "SSH"
        priority                   = 1001
        direction                  = "Inbound"
        access                     = "Allow"
        protocol                   = "Tcp"
        source_port_range          = "*"
        destination_port_range     = "22"
        source_address_prefix      = "*"
        destination_address_prefix = "*"
    }

    tags = {
        environment = "Terraform Demo"
    }
}

# Create network interface
resource "azurerm_network_interface" "myterraformnic" {
    name                      = "myNIC"
    location                  = "eastus"
    resource_group_name       = azurerm_resource_group.myterraformgroup.name

    ip_configuration {
        name                          = "myNicConfiguration"
        subnet_id                     = azurerm_subnet.myterraformsubnet.id
        private_ip_address_allocation = "Dynamic"
        public_ip_address_id          = azurerm_public_ip.myterraformpublicip.id
    }

    tags = {
        environment = "Terraform Demo"
    }
}

# Connect the security group to the network interface
resource "azurerm_network_interface_security_group_association" "example" {
    network_interface_id      = azurerm_network_interface.myterraformnic.id
    network_security_group_id = azurerm_network_security_group.myterraformnsg.id
}

# Generate random text for a unique storage account name
resource "random_id" "randomId" {
    keepers = {
        # Generate a new ID only when a new resource group is defined
        resource_group = azurerm_resource_group.myterraformgroup.name
    }

    byte_length = 8
}

# Create storage account for boot diagnostics
resource "azurerm_storage_account" "mystorageaccount" {
    name                        = "diag${random_id.randomId.hex}"
    resource_group_name         = azurerm_resource_group.myterraformgroup.name
    location                    = "eastus"
    account_tier                = "Standard"
    account_replication_type    = "LRS"

    tags = {
        environment = "Terraform Demo"
    }
}

# Create (and display) an SSH key
resource "tls_private_key" "example_ssh" {
  algorithm = "RSA"
  rsa_bits = 4096
}
output "tls_private_key" { 
    value = tls_private_key.example_ssh.private_key_pem 
    sensitive = true
}

# Create virtual machine
resource "azurerm_linux_virtual_machine" "myterraformvm" {
    name                  = "myVM"
    location              = "eastus"
    resource_group_name   = azurerm_resource_group.myterraformgroup.name
    network_interface_ids = [azurerm_network_interface.myterraformnic.id]
    size                  = "Standard_DS2_v2"

    os_disk {
        name              = "myOsDisk"
        caching           = "ReadWrite"
        storage_account_type = "Premium_LRS"
    }

    source_image_reference {
        publisher = "Canonical"
        offer     = "UbuntuServer"
        sku       = "18.04-LTS"
        version   = "latest"
    }

    computer_name  = "myvm"
    admin_username = "azureuser"
    disable_password_authentication = true

    admin_ssh_key {
        username       = "azureuser"
        public_key     = tls_private_key.example_ssh.public_key_openssh
    }

    boot_diagnostics {
        storage_account_uri = azurerm_storage_account.mystorageaccount.primary_blob_endpoint
    }

    tags = {
        environment = "Terraform Demo"
    }
}

img-description

Pipeline létrehozása

Létrehoztuk a szükséges terraform file-t az előző részben, a következő lépésként már magát a pipeline-t hozzuk létre, amely alkalmazza majd azt.
Hozzuk is létre a pipeline-t:

img-description
img-description
img-description
img-description
img-description

Töröljünk ki mindent az automatikus feltöltött file-ból, majd illesszük be a következőt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
trigger: none

pool:
  vmImage: ubuntu-latest


variables:
#Service connacetion neve amit létrehoztunk:
  - name: backendServiceArm
    value: 'Land3'
#RG neve amit létrehoztunk a state file számára    
  - name: backendAzureRmResourceGroupName
    value: 'gudszenttf'
#SA neve amit létrehoztunk a state file számára     
  - name: backendAzureRmStorageAccountName
    value: 'gudszenttf'
#Container neve amit létrehoztunk a state file számára az SA-n belül    
  - name: backendAzureRmContainerName
    value: 'tfstatedevops'    
  - name: backendAzureRmKey
    value: 'terraform.tfstate'
  - name: terraform_version
    value: '0.15.4'

stages :
  - stage: validate
    jobs:
    - job: validate
      continueOnError: false
      steps:
      - task: TerraformInstaller@0
        displayName: 'install'
        inputs:
          terraformVersion: ${{variables.terraform_version}}
      - task: TerraformTaskV2@2
        displayName: 'init'
        inputs:
          provider: 'azurerm'
          command: 'init'
          backendServiceArm: ${{variables.backendServiceArm }}
          backendAzureRmResourceGroupName: ${{variables.backendAzureRmResourceGroupName }}
          backendAzureRmStorageAccountName: ${{variables.backendAzureRmStorageAccountName }}
          backendAzureRmContainerName: ${{variables.backendAzureRmContainerName }}
          backendAzureRmKey: ${{variables.backendAzureRmKey }}
          workingDirectory: '$(System.DefaultWorkingDirectory)/Terraform'
      - task: TerraformTaskV2@2
        displayName: 'validate'
        inputs:
          provider: 'azurerm'
          command: 'validate'
          
  - stage: plan
    dependsOn: [validate]
    condition: succeeded('validate')
    jobs:
      - job: terraform_plan_develop
        steps:
        - task: TerraformInstaller@0
          displayName: 'install'
          inputs:
            terraformVersion: ${{variables.terraform_version}}
        - task: TerraformTaskV2@2
          displayName: 'init'
          inputs:
            provider: 'azurerm'
            command: 'init'
            backendServiceArm: ${{variables.backendServiceArm }}
            backendAzureRmResourceGroupName: ${{variables.backendAzureRmResourceGroupName }}
            backendAzureRmStorageAccountName: ${{variables.backendAzureRmStorageAccountName }}
            backendAzureRmContainerName: ${{variables.backendAzureRmContainerName }}
            backendAzureRmKey: ${{variables.backendAzureRmKey }}
            workingDirectory: '$(System.DefaultWorkingDirectory)/Terraform'
        - task: TerraformTaskV2@2
          displayName: 'plan'
          inputs:
            provider: 'azurerm'
            command: 'plan'
            environmentServiceNameAzureRM: ${{variables.backendServiceArm }}
            workingDirectory: '$(System.DefaultWorkingDirectory)/Terraform'

  - stage: apply
    dependsOn: [plan]
    condition: succeeded('plan')
    jobs:
      - job: terraform_apply_develop
        steps:
        - task: TerraformTaskV2@2
          displayName: 'init'
          inputs:
            provider: 'azurerm'
            command: 'init'
            backendServiceArm: ${{variables.backendServiceArm }}
            backendAzureRmResourceGroupName: ${{variables.backendAzureRmResourceGroupName }}
            backendAzureRmStorageAccountName: ${{variables.backendAzureRmStorageAccountName }}
            backendAzureRmContainerName: ${{variables.backendAzureRmContainerName }}
            backendAzureRmKey: ${{variables.backendAzureRmKey }} 
            workingDirectory: '$(System.DefaultWorkingDirectory)/Terraform'
        - task: TerraformTaskV2@2
          displayName: 'apply'
          inputs:
            provider: 'azurerm'
            command: 'apply'
            environmentServiceNameAzureRM: ${{variables.backendServiceArm }}
            workingDirectory: '$(System.DefaultWorkingDirectory)/Terraform' 

Commentek alapján cseréljük ki a változókat

img-description

Ha mindent jól csináltunk, akkor létre is jön a gépünk Azure-ban a hozzá tartozó erőforrásokkal együtt. img-description
!Fontos, ha már nem kell, amit korábban létrehoztunk, akkor töröljük is azt ;)

Ingyenes Azure DevOps parallel

Amennyiben még nem használtuk ez a környezet, és ezt a hibát tapasztaljuk:
img-description
Úgy igényelni kell ezen a formon keresztül futtató környezetet: Link
Pár óra után engedélyezik és már mehet is a tesztelés.(Régen automatikus volt, de gondolom páran visszaéltek vele…)

This post is licensed under CC BY 4.0 by the author.