Einführung
Terraform (IaaS - Infrastructure as a Service) und Ansible (CaaS - Configuration as a Service) bilden zusammen ein starkes Gespann, um schnell konfigurierte Ressourcen auf der Microsoft Azure Cloud zu provisionieren. In meinen bisherigen Blogartikeln habe ich bisher Terraform vorgestellt
und zwar wie mit Terraform effizient Ressourcen aus Azure DevOps (https://dev.azure.com) provisioniert werden können. Während sich Terraform vor allem im Bereich "Ressourcen-Deployment" etabliert hat, scheint sich Ansible im Bereich "Configuration as a Service" zu etablieren.
In diesem Artikel präsentiere ich ein Beispiel, wie mit Terraform Ansible Code - nach der Provisionierung des Servers - auf dem Server über einen SSH Tunnel ausgeführt werden kann. Nach Beendigung des Skripts, kann mittels eines VNC-Viewers auf den provisionierten Server in der Azure Cloud zugegriffen werden.
Obschon das Beispiel trivial ist, zeigt es doch wie Terraform und Ansible zusammen ein starkes Gespann bilden können, um beispielsweise innert Minuten eine vollständigen Webserver in der Azure Cloud zu deployen. Natürlich ist hier der Einwand berechtigt, dass Azure inzwischen andere, bessere Möglichkeiten vorsieht dies zu tun. Im Sinne einer transparenten Kostenbetrachtung ist es aber dennoch manchmal von Nutzen, eine Lösung wie sie im folgenden vorgestellt wird zu wählen.
Das Beispiel
Im Beispiel erstellen wir auf der Azure Cloud Plattform einen Linux Server (Ubuntu) mittels Terraform (hier noch Terraform 11). Nach der Erstellung der virtuellen Maschine - des Linux Servers - schreiben wir in eine Datei die öffentliche IP Adresse der VM und zusätzliche Konfigurationsparameter für Ansible. Wir führen dann mittels einer "Null-Ressource" Ansible als lokale Anwendung innerhalb Terraform aus. Dabei müssen wir stark darauf achten, dass die Ressourcen auch tatsächlich bei Ausführung von Ansible schon real vorhanden sind. Ansonsten läuft das Skript nicht durch.
Ressourcen mit Terraform generieren
Alle im nachfolgenden vorgestellten Codebeispiele befinden sich in einer Datei main.tf.
provider "azurerm" {
subscription_id = "your_subscription_id"
client_id = "your_client_id"
client_secret = "your_client_secret"
tenant_id = "your_tenant_id"
}
# Create a resource group if it doesn’t exist
resource "azurerm_resource_group" "myterraformgroup" {
name = "myResourceGroup"
location = "eastus"
}
# 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}"
}
# 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_prefix = "10.0.1.0/24"
depends_on = ["azurerm_virtual_network.myterraformnetwork"]
}
# Create public IPs
resource "azurerm_public_ip" "myterraformpublicip" {
name = "myPublicIP"
location = "eastus"
resource_group_name = "${azurerm_resource_group.myterraformgroup.name}"
allocation_method = "Dynamic"
}
# 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 = "*"
}
security_rule {
name = "VNC"
priority = 1500
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_ranges = ["5900","5901","5902","5903"]
source_address_prefix = "*"
destination_address_prefix = "*"
}
security_rule {
name = "VNC_out"
priority = 1500
direction = "Outbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "5900"
source_address_prefix = "*"
destination_address_prefix = "*"
}
}
# Create network interface
resource "azurerm_network_interface" "myterraformnic" {
name = "myNIC"
location = "eastus"
resource_group_name = "${azurerm_resource_group.myterraformgroup.name}"
network_security_group_id = "${azurerm_network_security_group.myterraformnsg.id}"
ip_configuration {
name = "myNicConfiguration"
subnet_id = "${azurerm_subnet.myterraformsubnet.id}"
private_ip_address_allocation = "dynamic"
public_ip_address_id = "${azurerm_public_ip.myterraformpublicip.id}"
}
depends_on = ["azurerm_network_security_group.myterraformnsg", "azurerm_public_ip.myterraformpublicip"]
}
Ganz oben im Code wird der Provider definiert, wo die Ressourcen deployt werden. Der Code ist so konzipiert, dass er aus der Azure Cloud Shell heraus läuft. Ich werde später noch darauf eingehen, welche Dateien in die Shell hochgeladen werden müssen, damit das Beispiel läuft. Anschliessend definieren wir eine Ressourcengruppe, die als Container für alle anderen Terraform Elemente dient. In die Ressourcengruppe legen wir folgende Elemente: Ein virtuelles Netzwerk, ein Subnet, eine zu generierende öffentliche IP und eine NSG. In der NSG definieren wir die Firewall-Rules, also den möglichen Zugriff auf die später zu erzeugende virtuelle Maschine. Wir öffnen den Port 22 (SSH) und einige VNC Ports für den VNC Zugriff auf den Rechner. In einem nächsten Schritt (immer alles noch in der Datei main.tf) definieren wir einen Zufallsnamen für den Storage Account (dieser muss weltweit eindeutig sein):
# 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 = "${random_id.randomId.hex}"
resource_group_name = "${azurerm_resource_group.myterraformgroup.name}"
location = "eastus"
account_tier = "Standard"
account_replication_type = "LRS"
depends_on = ["azurerm_network_interface.myterraformnic"]
}
Danach können wir die virtuelle Maschine definieren:
In einem nächsten Schritt fragen wir die öffentliche IP ab und erzeugen das Repository für Ansible mit Terraform:
# Create virtual machine
resource "azurerm_virtual_machine" "myterraformvm" {
name = "myVM"
location = "eastus"
resource_group_name = "${azurerm_resource_group.myterraformgroup.name}"
network_interface_ids = ["${azurerm_network_interface.myterraformnic.id}"]
vm_size = "Standard_DS1_v2"
storage_os_disk {
name = "myOsDisk"
caching = "ReadWrite"
create_option = "FromImage"
managed_disk_type = "Premium_LRS"
}
storage_image_reference {
publisher = "Canonical"
offer = "UbuntuServer"
sku = "16.04-LTS"
version = "latest"
}
os_profile {
computer_name = "myvm"
admin_username = "azureuser"
admin_password = "Password1234!"
}
os_profile_linux_config {
disable_password_authentication = false
}
boot_diagnostics {
enabled = "true"
storage_uri = "${azurerm_storage_account.mystorageaccount.primary_blob_endpoint}"
}
depends_on = ["azurerm_storage_account.mystorageaccount", "azurerm_network_interface.myterraformnic"]
}
In einem nächsten Schritt fragen wir die öffentliche IP ab und erzeugen das Repository für Ansible mit Terraform:
data "azurerm_public_ip" "test" {
name = "myPublicIP"
resource_group_name = "${azurerm_resource_group.myterraformgroup.name}"
depends_on = ["azurerm_resource_group.myterraformgroup", "azurerm_public_ip.myterraformpublicip","azurerm_virtual_machine.myterraformvm"]
}
data "template_file" "ansible-setup" {
template = "[test]\n$${ip}\n[test:vars]\n$${connection}\n$${user}\n$${ssh}\n$${args}\n"
vars = {
ip = "${data.azurerm_public_ip.test.ip_address}"
connection = "ansible_connection=ssh"
user= "ansible_user=azureuser"
ssh = "ansible_ssh_pass=Password1234!"
#interpreter = "ansible_python_interpreter=/usr/bin/python3"
args ="ansible_ssh_common_args='-o StrictHostKeyChecking=no'"
}
depends_on=["azurerm_virtual_machine.myterraformvm","data.azurerm_public_ip.test"]
}
Das Template müssen wir noch als Datei rendern:
resource "local_file" "foo" {
content = "${data.template_file.ansible-setup.rendered}"
filename = "inventory.txt"
depends_on = ["data.template_file.ansible-setup"]
}
Abhängigkeiten
Folgendes muss noch eingefügt werden, wenn das Beispiel mit einer Pipeline über Azure DevOps ausgeführt wird:
resource "null_resource" "install-deps" {
provisioner "local-exec" {
working_dir = "."
command = "sudo apt-get install sshpass"
}
depends_on= ["local_file.foo"]
}
Wird das Beispiel aus der Cloud-Shell deployt ist obiger Schritt nicht notwendig. Dafür muss SSHPass auf der Cloud Shell installiert werden. Das ist nur mit einigem Zusatzaufwand möglich:
1) Verbinden auf die Cloud-Shell:
2) Erstellen eines Verzeichnisses sshpass (mkdir sshpass)
3) Installieren von sshpass mittels wget:
wget https://downloads.sourceforge.net/project/sshpass/sshpass/1.06/sshpass-1.06.tar.gz
4) Un-taren:
tar zxvf sshpass-1.06.tar.g
5) Ins Verzeichnis wechseln, wo sshpass un-tared wurde
6) ./configure ausführen
7) make
8) ./sshpass kann nun ausgeführt werden, jedoch wollen wir es in der Pfad Variable:
9) Modifikation PATH Env. Variable: PATH=$HOME/pfad zu sshpass:$PATH
sshpass kann so installiert werden. Die Vorgehensweise ist aber sicherlich nicht ganz sauber!
Ansible ausführen
resource "null_resource" "run_ansible" {
provisioner "local-exec" {
working_dir = "."
command = "ansible-playbook -i inventory.txt test_playbook.yml"
}
depends_on= ["null_resource.install-deps"]
}
Ansible konfigurieren
Bevor wir das Beispiel mit Terraform ausführen können, müssen wir noch das Ansible Playbook verfassen. Dies tun wir im folgenden.
Hinweis: Da ich kein Ansible Profi bin, gibt es sicherlich bessere Varianten, dass Playbook zu schreiben:
Der folgende Inhalt kommt in eine Datei test_playbook.yml
- hosts: all
tasks:
- name: apt install
become: yes
apt:
name: "{{ item.name }}"
state: "{{ item.state }}"
with_items:
- { state: "latest", name: "xfce4" }
- name: Install the GUI and VNC Packages
become: yes
apt:
name: "{{ item.name }}"
state: "{{ item.state }}"
with_items:
- { state: "latest", name: "vnc4server"}
- name: Install Dos 2 Unix converter
become: yes
apt:
name: "{{ item.name }}"
state: "{{ item.state }}"
with_items:
- { state: "latest", name: "dos2unix"}
- name: Set the vnc-password
shell: 'rm mypasswd.txt | vncpasswd < mypasswd.txt | echo "mypass" >> mypasswd.txt | echo "mypass" >> mypasswd.txt'
- name: Copy the vncservers.conf file in /etc/ directory
become: yes
template:
src: vncserver.conf
dest: /etc/vncservers.conf
owner: root
group: root
- name: Copy the modified "xstartup" file
template:
src: xstartup.txt
dest: .vnc/xstartup
mode: 0755
- name: Move vncserver script to init.d
become: yes
template:
src: vncserver
dest: /etc/init.d/vncserver
owner: root
group: root
mode: 0755
register: vnc_service
- name: Convert vncserver script to unix format
become: yes
shell: 'dos2unix /etc/init.d/vncserver'
- name: Convert vncserver xstartup to unix format
become: yes
shell: 'dos2unix .vnc/xstartup'
- name: Add vncserver service to default runlevels
become: yes
command: "update-rc.d vncserver defaults"
when: vnc_service.changed
- name: Restart VNC Service
become: yes
service:
name: vncserver
pattern: /etc/init.d/vncserver
state: restarted
Beschreibung der einzelnen Schritte
GDM installieren
In einem ersten Schritt installieren wir XFCE als leichtgewichtigen Window Manager unter Linux. In diesem Schritt können auch andere Windows Manager installiert werden. Allerdings muss darauf geachtet werden, dass der Window Manager "headless", das heisst ohne Monitor betrieben werden kann:
- name: apt install
become: yes
apt:
name: "{{ item.name }}"
state: "{{ item.state }}"
with_items:
- { state: "latest", name: "xfce4" }
VNC Server installieren
- name: Install the GUI and VNC Packages
become: yes
apt:
name: "{{ item.name }}"
state: "{{ item.state }}"
with_items:
- { state: "latest", name: "vnc4server"}
Optional Dos2Unix Konverter
Manchmal gibt es Probleme bei Dateien, die unter Windows erfasst werden unter Linux. Dem kann Abhilfe geschaffen werden mit dos2unix.
- name: Install Dos 2 Unix converter
become: yes
apt:
name: "{{ item.name }}"
state: "{{ item.state }}"
with_items:
- { state: "latest", name: "dos2unix"}
VNC Passwort setzen
!! Dies sollte definitiv in einer produktiven Umgebung nicht so gemacht werden!!
- name: Set the vnc-password
shell: 'rm mypasswd.txt | vncpasswd < mypasswd.txt | echo "mypass" >> mypasswd.txt | echo "mypass" >> mypasswd.txt'
Kopieren von Konfigurationsdateien
Damit das ganze artig läuft, müssen wir einige Konfigurationsdateien für den VNC Server rüber auf die frisch deployte VM kopieren:
Der Inhalt der Dateien ist unten angegeben:
- name: Copy the vncservers.conf file in /etc/ directory
become: yes
template:
src: vncserver.conf
dest: /etc/vncservers.conf
owner: root
group: root
- name: Copy the modified "xstartup" file
template:
src: xstartup.txt
dest: .vnc/xstartup
mode: 0755
vncservers.conf:
<<to be continued>>
Init.d initialisieren
Hier muss init.d so initialisiert werden, so dass bei Neustart der VM, der VNC Server automatisch wieder gestartet wird:
- name: Move vncserver script to init.d
become: yes
template:
src: vncserver
dest: /etc/init.d/vncserver
owner: root
group: root
mode: 0755
register: vnc_service
- name: Convert vncserver script to unix format
become: yes
shell: 'dos2unix /etc/init.d/vncserver'
- name: Convert vncserver xstartup to unix format
become: yes
shell: 'dos2unix .vnc/xstartup'
- name: Add vncserver service to default runlevels
become: yes
command: "update-rc.d vncserver defaults"
when: vnc_service.changed
Und am Schluss den VNC Server neu starten:
- name: Restart VNC Service
become: yes
service:
name: vncserver
pattern: /etc/init.d/vncserver
state: restarted
Wenn das ganze durchläuft, so kann man sich dann bequem via zum Beispiel Ultra-VNC Viewer auf einen Desktop in Azure verbinden.
Keine Kommentare:
Kommentar veröffentlichen