Как сделать windows display на русском

Ansible Vault is a pretty nifty tool that allows people to easily encrypt secrets for use in Ansible. It’s a builtin tool that can be use to encrypt secrets and make them easily usable for Ansible.

When calling Ansible, you must supply the password by either manually entering it or use a password file. From there, Ansible will automatically decrypt the contents when it is required, simple stuff.

As an example I can turn something like this

host_password: supersecretpass123!

into this

$ANSIBLE_VAULT;1.1;AES256
33343835306666636239373663396363643766613363343837646633343933376633323964663030
3134616235646661306436643134383333633730376233650a663466323032343633383061336461
36393261363338616337613039363435313631343437323164386661326633313339396238396236
3462393338636632650a653036663266373533343232393838343161396564333963643632653932
30386135636131656130346537356637396139323134386162306431376564346537633566666532
6331323061373237336639356165393563613765663864366231

As a dev on the Ansible project, this was an area that I didn’t really understand and I wanted to fix that by understanding how the vault works. At the same time I also wanted to have another way for people working on Windows to encrypt and decrypt vault files, reducing the barrier for using Ansible for those users. Currently Ansible, and by extension ansible-vault, is unable to run natively on Windows. There are ways around this such as using WSL or Cygwin to run the Ansible scripts but either this won’t work for all users (WSL) or is quite complex to setup (Cygwin).

I decided to try and kill 2 birds with 1 stone, which was to re implement the functionality of ansible-vault in another language. In doing so I gain an understanding of how ansible-vault works as well as giving Windows users a native solution for managing vault files.

Before I started to write this new tool, I had a few goals in mind which were;

  • Runs natively on Windows with no extra software dependencies
  • Also works on older Windows hosts like Windows 7, 8, and 8.1
  • Simple and easy to setup/use
  • Easy to maintain the software going forward

I could have just ported the Python code to a script that works on Windows and be done with it but, in my mind, that would be cheating and doesn’t really fulfill all my goals. Specifically it would require Python to be installed (not a hard ask I know but still extra software) and I wouldn’t be learning too much if I just reused code from the Ansible codebase.

The result is a PowerShell module that includes cmdlets to encrypt and decrypt vault files but before I go into the PowerShell side I want to explain how Ansible Vault works based on what I learnt.

Analysing Ansible Vault

The code behind Ansible Vault is all open source and can viewed in the Ansible GitHub Repo by anyone. At the time of writing this blog post, AES256 at the 1.1/1.2 implementation was the latest and that is what this section will break down. Each vault is split into 2 main parts;

  • Header – On the first line of the vault file and defines the structure of the vault
  • Cipher text – Contains the salt, hmac hash, and the encrypted bytes as a hex string with a newline at every 80 chars

Header

The header of the vault is comprised of a few keys fields, each separated by ;, which are;

  • The file format ID, currently only $ANSIBLE_VAULT is used
  • The vault version that indicates how it was encrypted
  • The cipher used for encryption
  • An optional ID field

Versions

There are 3 versions of the Ansible Vault that exist;

  • 1.0 – Introduced in Ansible 1.5, this is the original vault format and no longer in use
  • 1.1 – Introduced in Ansible 1.5.1, fixed issues with the original format and is still in use today
  • 1.2 – Introduced in Ansible 2.4, is the same as 1.1 but includes the ID field in the header

Currently 1.1 is used when no ID is set to the vault and 1.2 is used when an ID set. The cipher contents and encryption process are the same between the two, the only difference is the extra field in the header. Because the 1.0 format was quickly removed in a single minor release, I did not implement support for decrypting that format. If you still have a vault file in that format, change it immediately!

Ciphers

The next field in the header is the cipher that is used, currently it can be either of the following;

  • AES – Used in the 1.0 version and no longer in use
  • AES256 – Used in both 1.1 and 1.2, is based on the AES cipher with a 256 bit key in CTR mode

This made thing relatively simple as there is only cipher I needed to add support for.

Vault ID

The vault ID was added in the 1.2 version and it is used by Ansible to map a password to a particular vault file. For example, a user can create a vault file for each environment, say dev, uat, prod, and create a vault file for each environment with different passwords per ID/environment. When a vault file contains an ID, the header would look like the following;

$ANSIBLE_VAULT;1.2;AES256;prod

Cipher Text

Before breaking down the cipher text, I will briefly cover the various cryptography tools used as part of the vault process. Currently these functions/protocls are being used;

  • AES block cipher with a 32-byte (256 bit) key in CTR mode to encrypt/decrypt
  • PBKDF2 using HMAC SHA256 to derive the various keys, this takes in a 32-byte salt, runs for 10000 iterations
  • HMAC using SHA256 to verify the KDF output against the encrypted bytes
  • PKCS7 padding on the encrypted bytes

When bringing this all together we need the following bits of information;

  • A password used as the secret input into the PBKDF2 function
  • A 32-byte salt to use with the password in the PBKDF2 function
  • A 32-byte key to use as part of the HMAC function
  • A 32-byte key used to initialise the AES cipher
  • A 16-byte key used as the base counter/nonce of the AES CTR mode

We already know the password and the salt is stored in the cipher text, from there we can get the rest of the keys as they are the output of the PBKDF2 function. To get the salt, lets first break down the cipher text (excluding the header). Here is a sample cipher text that I posted in the beginning of this blog;

33343835306666636239373663396363643766613363343837646633343933376633323964663030
3134616235646661306436643134383333633730376233650a663466323032343633383061336461
36393261363338616337613039363435313631343437323164386661326633313339396238396236
3462393338636632650a653036663266373533343232393838343161396564333963643632653932
30386135636131656130346537356637396139323134386162306431376564346537633566666532
6331323061373237336639356165393563613765663864366231

This is a hex encoded string that contains the salt, the SHA256 based HMAC output of the encrypted bytes, and finally the encrypted bytes. Each entry is split by a newline which in hex form is 0a, applying this split this is what we get;

# Salt
33343835306666636239373663396363
64376661336334383764663334393337
66333239646630303134616235646661
30643664313438333363373037623365

# HAMC
66346632303234363338306133646136
39326136333861633761303936343531
36313434373231643866613266333133
39396238396236346239333863663265

# Encrypted Bytes
65303666326637353334323239383834
31613965643339636436326539323038
61356361316561303465373566373961
39323134386162306431376564346537
63356666653263313230613732373366
39356165393563613765663864366231

Each entry is actually a hex string of a hex string, I’m not sure why this has been hex encoded twice but that’s just how it is. When “un-hexified” we get the following hex values for each entry;

# Salt (32 bytes)
34850ffcb976c9ccd7fa3c487df34937
f329df0014ab5dfa0d6d14833c707b3e

# HMAC (32 bytes)
f4f20246380a3da692a638ac7a096451
6144721d8fa2f31399b89b64b938cf2e

# Encrypted Bytes (48 bytes)
e06f2f75342298841a9ed39cd62e9208
a5ca1ea04e75f79a92148ab0d17ed4e7
c5ffe2c120a7273f95ae95ca7ef8d6b1

Bingo, we know have the salt and password and can use that in conjunction with the PBKDF2 function to produce the remaining keys. PBKDF2 is a key derivation function that applies a pseudorandom function to a secret input, such as a password, along with a salt to produce a derived key. Part of this function is the ability to specify the number of iterations that repeats the process to make the computational cost of calculating the key more expensive. As shown on Wikipedia, PBKDF2 has five input parameters

DK = PBKDF2(PRF, Password, Salt, c, dkLen)

where:

  • PRF: is a pseudorandom function like a HMAC, for Ansible Vault this uses the SHA256 algorithm
  • Password: the password/secret that is known to the user
  • Salt: the salt, this is randomly generated when creating the vault but stored as part of the cipher text of an existing vault as we saw above
  • c: the number of iterations desired, Ansible Vault is set to 10000
  • dkLen: the length of output key, for Ansible Vault we want 80 as (80 == (32 + 32 + 16) == (AES Key + HMAC Key + CTR Counter/Nonce))

Putting this into practice, you can use this handy Python script to generate the derived key. Note: for this to work you need the cryptography package installed;

import binascii

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC

password = b"password"  # this is the password I used to create the example vault
kdf = PBKDF2HMAC(
    algorithm=hashes.SHA256(),
    length=80,
    salt=binascii.unhexlify("34850ffcb976c9ccd7fa3c487df34937f329df0014ab5dfa0d6d14833c707b3e"),
    iterations=10000,
    backend=default_backend())
derived_key = kdf.derive(password)
print(binascii.hexlify(derived_key))

The output of this script is the following hex string;

# KDF (80 bytes)
fc4a21fb71bfaad6a0bbb078f0704721
ccad80519fc349c3ff14268fced14203
9bfb1a43effdfb8f8d7119387fccec54
8859c7fccc26589a65a2ee856e05763f
394f9f4a44152b33234cba44c930921b

Ansible Vault, splits this output at 32 and 64 bytes into 3 parts like so;

# AES Key (32 bytes)
fc4a21fb71bfaad6a0bbb078f0704721
ccad80519fc349c3ff14268fced14203

# HMAC Key (32 bytes)
9bfb1a43effdfb8f8d7119387fccec54
8859c7fccc26589a65a2ee856e05763f

# AES CTR Counter/Nonce (16 bytes)
394f9f4a44152b33234cba44c930921b

Now we know all the keys that are required to decrypt/encrypt the bytes but before we do that, we want to verify the HMAC output against what is expected. If the HMAC value does not match the expectation, we know the password/secret was incorrect and can report that back to the user. Using a simple Python script we can get the HMAC value of the encrypted bytes

import binascii
import hashlib
import hmac

key = binascii.unhexlify("9bfb1a43effdfb8f8d7119387fccec548859c7fccc26589a65a2ee856e05763f")
encrypted_bytes = binascii.unhexlify("e06f2f75342298841a9ed39cd62e9208a5ca1ea04e75f79a92148ab0d17ed4e7c5ffe2c120a7273f95ae95ca7ef8d6b1")

hmac_digest = hmac.new(key, encrypted_bytes, digestmod=hashlib.sha256).digest()
print(binascii.hexlify(hmac_digest))

This produces the result f4f20246380a3da692a638ac7a0964516144721d8fa2f31399b89b64b938cf2e which matches the HMAC value stored in the vault. We know the secret was correct and we can move onto decrypting the bytes themselves.

To decrypt the bytes, we need to use AES in CTR mode, here is some Python code to decrypt the data;

import binascii
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

key = binascii.unhexlify("fc4a21fb71bfaad6a0bbb078f0704721ccad80519fc349c3ff14268fced14203")
nonce = binascii.unhexlify("394f9f4a44152b33234cba44c930921b")
encrypted_bytes = binascii.unhexlify("e06f2f75342298841a9ed39cd62e9208a5ca1ea04e75f79a92148ab0d17ed4e7c5ffe2c120a7273f95ae95ca7ef8d6b1")

aes_cipher = Cipher(algorithms.AES(key), modes.CTR(nonce), default_backend())
decryptor = aes_cipher.decryptor()
plaintext = decryptor.update(encrypted_bytes) + decryptor.finalize()
print(plaintext)

This produces the following UTF-8 string;

host_password: supersecretpass123!\n\r\r\r\r\r\r\r\r\r\r\r\r\r

So close to the plaintext but now we have \r padded onto the end of the string. This is just simple PKCS7 padding to make sure the input to the AES cipher is a multiple of the AES block size (16 bytes). This padding is the same byte where the decimal value of that byte is the number of bytes needed. In the case above, the length of the plaintext value (including the last newline) is 35 while the nearest full block is 48. To reach the full block size of 48, the byte with a decimal value of 13 (\r) needs to be appended. After removing the padding we get the original plaintext which Ansible loads as required.

To encrypt a string, it’s pretty much the same process in reverse order. For example this is what Ansible does when it goes to encrypt a string;

  1. Pad the text using the PKCS7 mechanism to the block size of 16 (AES block size / 8)
  2. Generate a secure random 32-byte salt to use in the PBKDF2 function
  3. Generate the AES, HMAC, and AES CTR counter/nonce bytes using the PBKDF2 function with the salt and password specified
  4. Encrypt the padded text text using AES in CTR mode with the keys produced in step 3
  5. Generate the HMAC SHA256 hash of the encrypted bytes based on the HMAC key from step 3
  6. Create a hex string of the salt, HMAC hash, and encrypted bytes
  7. Join the three hex string’s with a newline
  8. Create another hex string of the value from step 7
  9. Create the vault header and append the value from step 8

From there you have an Ansible Vault that is securely protected by a password of your choosing.

Working with Windows

So now we know how the vault works and how Ansible encrypts/decrypts the data, it should be fairly trivial to reimplement it in PowerShell, or so I thought.

Problems on Windows

The biggest issue I had with Windows was finding a way to interop with the functions that are being used in the Ansible Vault process. More specifically I needed a way to;

  • Use PBKDF2 with the SHA256 algorithm
  • Use AES in CTR mode
  • Pad and unpad the decrypted bytes with the PKCS7 spec

Each one of the above is either not possible in the standard .NET framework that PowerShell uses, or requires a newer version of the framework. Let’s break down each part and how I ultimately implemented it in PowerShell.

PBKDF2 with SHA256

The first step in implementing Ansible Vault on Windows is to derive the keys used in the downstream process. A quick Google search on how this can be done leads me to the Rfc2898DeriveBytes class. The description for this class is

Implements password-based key derivation functionality, PBKDF2, by using a pseudo-random number generator based on HMACSHA1.

Unfortunately we need to the use SHA256 hash in our generator so the default of SHA1 won’t produce the right key that we need. There’s some constructors that have a HashAlgorithmName parameter so that looks like what we need.

What the, the docs says there is!

So that didn’t work, when reading the docs around that constructor I found this gem

Version Information
.NET Framework
Available since 4.7.2

So I either need to set a dependency on .NET 4.7.2 to run the cmdlets or find some other way around this limitation, because my goal was to support older hosts I pretty much had to find another way. The fact that a newer version of the .NET framework exposes the parameter that I needed usually means Windows can do it with the native Win32 APIs. After some searching I came across this blog post from Microsoft which used the BCryptKeyDerivation Win32 API. This is close to what I am looking for but unfortunately BCryptKeyDerivation was only added in Windows 8/Server 2012 and I was hoping for something I can use in Windows 7/Server 2008 R2. Finally I came across the BCryptDeriveKeyPBKDF2 which was added in Windows 7/Server 2008 R2.

So now we have an API available on Windows that can give us the key required but we need a way to call this within PowerShell. There’s no inbuilt way in PowerShell to do this, but you can use C# code with P/Invoke to call these platform functions and PowerShell can compile/run C# code. This is done through the Add-Type cmdlet which calls csc.exe to compile the C# code and load it into the current PowerShell scope. For example here is a simple way to add the functions we need in PowerShell;

Add-Type -Namespace Win32 -Name NativeFunctions -MemberDefinition @'
[DllImport("Bcrypt.dll")]
public static extern uint BCryptOpenAlgorithmProvider(
    out IntPtr phAlgorithm,
    [MarshalAs(UnmanagedType.LPWStr)] string pszAlgId,
    [MarshalAs(UnmanagedType.LPWStr)] string pszImplementation,
    UInt32 dwFlags);

[DllImport("Bcrypt.dll")]
public static extern uint BCryptDeriveKeyPBKDF2(
    IntPtr hPrf,
    [MarshalAs(UnmanagedType.LPWStr)] string pbPassword,
    UInt32 cbPassword,
    byte[] pbSalt,
    UInt32 cbSalt,
    UInt64 cIterations,
    byte[] pbDerivedKey,
    UInt32 cbDerivedKey,
    UInt32 dwFlags);

[DllImport("Bcrypt.dll")]
public static extern uint BCryptCloseAlgorithmProvider(
    IntPtr hAlgorithm,
    UInt32 dwFlags);
'@

$algo = [IntPtr]::Zero
$res = [Win32.NativeFunctions]::BCryptOpenAlgorithmProvider([Ref]$algo, "SHA256", $null, 0x00000008)
if ($res -ne 0) {
    throw "Failed to open algo provider"
}

$key = New-Object -TypeName byte[] -ArgumentList 80
$salt = New-Object -TypeName byte[] -ArgumentList 32
$pass = "password"
$res = [Win32.NativeFunctions]::BCryptDeriveKeyPBKDF2($algo, $pass, $pass.Length, $salt, $salt.Length, 10000, $key, $key.Length, 0)
if ($res -ne 0) {
    throw "Failed to derive key"
}

[Win32.NativeFunctions]::BCryptCloseAlgorithmProvider($algo, 0)

Using Add-Type does have some downsides, it creates a temporary DLL file on disk and it takes more time to compile. None of these are really issues for this scenario but it has been something I wanted to work around for some time. I decided to take the opportunity to find another way around Add-Type and ultimately came across a way of calling these native functions with .NET Reflection. There are some blog posts which I have referenced at the bottom of this post, that helped me to understand how to use reflection. This resulted in the following cmdlet Invoke-Win32Api which can be used to call any Win32 APIs. If you are interested in learning more about this I recommend you look at the code and references to see how it works.

In the end, New-PBKDF2Key is what I ended up with. It calls the native Win32 APIs to produce the key required in a way that works on Windows 7/Server 2008 R2 and newer. In the future, I may add a conditional check to use Rfc2898DeriveBytes if the HashAlgorithmName constructor is available so this works on .NET Core but that wasn’t part of my original goals.

AES in CTR mode

Now that we have solved the PBKDF2 issue we move onto the next one, getting the AesCryptoServiceProvider to work in CTR mode. When looking at the CipherMode enumeration values, there is no option to run in CTR mode which is going to be a problem for us.

Looking at the underlying Win32 functions I was hoping this would be a similar situation as the PBKDF2 with SHA256 but unfortunately that didn’t seem to be the case, I had to implement this mode myself. I was loath to do this and was prepared to scrap this whole idea but luckily CTR mode isn’t that complex to do. From my understanding (please don’t reference me for this), it works like this;

  1. A counter is initialised from a randomly unique nonce value, this nonce is derived as part of the key from the PBKDF2 function (last 16 bytes)
  2. The counter is then encrypted with the AES cipher in ECB mode, this AES cipher is based on the 32-byte key from the PBKDF2 function (first 32 bytes)
  3. The counter is incremented by 1
  4. The result from step 2 is the XOR’d with the plaintext or ciphertext byte by byte until there are no more bytes left in the output
  5. Steps 2-4 is repeated until all bytes in the plaintext or cipher text have been XOR’d

Taking this into practice, let’s decrypt the first 32 bytes of our example cipher text, here are the inputs from the example;

# First 16 bytes of our encrypted bytes
e06f2f75342298841a9ed39cd62e9208

# Next 16 bytes of our encrypted bytes
a5ca1ea04e75f79a92148ab0d17ed4e7

# The starting 16-byte counter/nonce (from the PBKDF2 function)
394f9f4a44152b33234cba44c930921b

# The 32-byte AES key used in counter transformations
fc4a21fb71bfaad6a0bbb078f0704721ccad80519fc349c3ff14268fced14203

When using a site like this online AES ECB cipher we can encrypt the current counter value with the AES 256 key to get the input to the XOR function.

When xoring the output 88005c016b52f9f769e9bceeb214b27b with the next 16 bytes in the encrypted bytes array e06f2f75342298841a9ed39cd62e9208 we get 686f73745f70617373776f72643a2073 which is host_password: s in UTF-8. Now we have exhausted the XOR input, we need to increment the counter and repeat the process again.

# Previous counter
394f9f4a44152b33234cba44c930921b

# Next counter
394f9f4a44152b33234cba44c930921c

# AES ECB encryption output of this counter
d0ba7bd23d1094e8f760fad1a20de5d5

# Next 16 bytes of our encrypted bytes to XOR
a5ca1ea04e75f79a92148ab0d17ed4e7

# XOR result
75706572736563726574706173733132

# UTF-8 string of the XOR hex string
upersecretpass12

Putting the outputs together we currently have host_password: supersecretpass12. there are still some encrypted bytes left but the process is still the same; increment the counter, encrypt counter, XOR encrypted result with the next encrypted block. Putting this into PowerShell was relatively simple and the end result is Invoke-AESCTRCycle. Because of the nature of XOR, the encryption and decryption process is exactly the same in CTR mode.

PKCS7 padding

So we have solved the hurdle of PBKDF2 with SHA256 and adding support for AES in CTR mode, the last remaining hurdle is adding a function to pad and unpad our bytes based on the PKCS7 algorithm. Usually padding is used to “pad” the input bytes so it fits the block size in a cipher, e.g. AES uses a block size of 16 so each input to the transformation process must also be 16-bytes. AES in CTR mode is actually a stream cipher so the input block does not need to be the same size, but in Ansible Vault, the data is still padded.

Usually this padding is done as part of the AesCryptoServiceProvider but because we implemented our own method and CTR mode is not a block cipher we need to manually pad or unpad the data. Luckily PKCS7 is a relatively simple algorithm and easily implement, it appends the same byte that is equal to the number of bytes to add for a complete block until the block is complete. The exception to this is if the input data is currently the size of the block, PKCS7 will still add another block with the value being the number of bytes added.

Here are some examples of padding in action for an 8-byte block size;

# 8 - 1 = 7, the byte with the value 07, is added 7 times
01 == 01 07 07 07 07 07 07 07

# 8 - 3 = 5, the byte with the value 05 is added 5 times
01 02 03 == 01 02 03 05 05 05 05 05

# When the input is the same as the block size, an extra block is added
01 02 03 04 05 06 07 08 == 01 02 03 04 05 06 07 08 08 08 08 08 08 08 08 08

I ended up with 2 cmdlets to achieve this Add-Pkcs7Padding, and Remove-Pkcs7Padding.

AnsibleVault module

Putting this all together, I present AnsibleVault, a PowerShell module that can encrypt and decrypt Ansible vaults on Windows. Earlier I stated that one of my goals was to have a way to easily maintain the script and just creating a module does not fully fit this. I wanted a way to automatically;

  • Run sanity checks on the code
  • Run unit and integration tests
  • Automatically deploy the changes to the PowerShell Gallery

To achieve this, I used a combination of the following PowerShell modules;

  • PsScriptAnalyzer – static code checker
  • Pester – testing and code coverage tool
  • PSDeploy – tool used for simplifying the module deployment to PowerShell Gallery
  • Psake – build automation tool
  • BuildHelpers – helper tool for running a build in a CI environment like Appveyor

If you interested in how I put all of this together I highly recommend you read through this blog post who is the author of some of those modules. I implemented most of the work that the blog talked through but added a few extra things like the PsScriptAnalyzer and code coverage steps which are two metrics I am interested in.

Ultimately I can test out changes to AnsibleVault and have a system that will reliably run tests and other checks on the changes automatically. My only wish would be a way to test against older PowerShell versions and not just one but that’s probably a project for another time. The other great thing about this workflow is that to deploy a new release to the PowerShell Gallery, all I need to do is create a new tag in GitHub. This will kick off a run in Appveyor which will publish the changes to the gallery.

How to get it

Now that the module is part of the PowerShell Gallery it is quite simple to install, just run the Install-Module module.

Note: this will only work if you are running PowerShell v5 or have PowerShellGet installed for older versions.

You can set -Force to automatically accept prompt

If you don’t want to install the module system wide (or you don’t have admin rights), you can install it just for the current user by setting -Scope CurrentUser on the install cmdlet. PowerShellGet makes managing modules such a simple thing to do and I highly recommend people install it through the MSI if they can’t upgrade PowerShell to version 5. If you wish to uninstall the module, the cmdlet Uninstall-Module -Name AnsibleVault can be used to remove it from the system.

If you don’t want to install PowerShellGet you can manually install it on the system by doing the following;

  1. Download the latest zip from GitHub here
  2. Extract the zip
  3. Copy the folder AnsibleVault inside the zip to a path that is set in $env:PSModulePath, e.g. C:\Program Files\WindowsPowerShell\Modules or C:\Users\<user>\Documents\WindowsPowerShell\Modules
  4. Trust the downloaded files with $path = (Get-Module -Name AnsibleVault -ListAvailable).ModuleBase; Unblock-File -Path $path\*.psd1; Unblock-File -Path $path\Public\*.ps1; Unblock-File -Path $path\Private\*.ps1
  5. Restart PowerShell so the unblock policy is applied to the module

Note: You are not limited to those paths, you can add a new entry to the environment variable PSModulePath if you want to use another path.

Here I have installed it under the user’s profile

Using the AnsibleVault module

Now that the module is installed, the cmdlets Get-DecryptedAnsibleVault and Get-EncryptedAnsibleVault can be used like any other cmdlet. Here is the basic syntax for each module;

Get-DecryptedAnsibleVault `
    [-Value] <String> `
    [-Password] <SecureString> `
    [-Encoding <Encoding>] `
    [<CommonParameters>]

Get-DecryptedAnsibleVault `
    [-Path] <String> `
    [-Password] <SecureString> `
    [-Encoding <Encoding>] `
    [<CommonParameters>]

Get-EncryptedAnsibleVault `
    [-Value] <String> `
    [-Password] <SecureString> `
    [-Id <String>] `
    [<CommonParameters>]

Get-EncryptedAnsibleVault `
    [-Path] <String> `
    [-Password] <SecureString> `
    [-Id <String>] `
    [<CommonParameters>]

Here are what each parameter does;

  • Value: A string value to encrypt/decrypt, this can also be sent as a pipeline input
  • Path: The path to a file whose contents will be encrypted/decrypted
  • Password: A secure string that is the password for the vault
  • Encoding: When decrypting a vault, this is the final output string encoding of the vault (default if UTF-8). You shouldn’t have to touch this parameter but if the source vault file was not UTF-8 than this can control the final decrypted output
  • Id: An optional parameter that specifies the ID to assign to the new vault string

You can use a combination of these cmdlets and pipelining to achieve similar results to some common ansible-vault commands, here are some common examples to replace existing ansible-vault functionality;

# ansible-vault encrypt C:\temp\vault.yml --ask-vault-pass
Get-EncryptedAnsibleVault -Path C:\temp\vault.yml | Set-Content -Path C:\temp\vault.yml

# ansible-vault encrypt C:\temp\vault.yml --vault-id dev@prompt
Get-EncryptedAnsibleVault -Path C:\temp\vault.yml -Id dev | Set-Content -Path C:\temp\vault.yml

# ansible-vault rekey C:\temp\vault.yml --ask-vault-pass
Get-DecryptedAnsibleVault -Path C:\temp\vault.yml | Get-EncryptedAnsibleVault | Set-Content -Path C:\temp\vault.yml

# ansible-vault view C:\temp\vault.yml --ask-vault-pass
Get-DecryptedAnsibleVault -Path C:\temp\vault.yml

# ansible-vault decrypt C:\temp\vault.yml --ask-vault-pass
Get-DecryptedAnsibleVault -Path C:\temp\vault.yml | Set-Content -Path C:\temp\vault.yml

# ansible-vault encrypt_string --stdin-name 'vault_variable'
$vault_text = Read-Host -Prompt "Enter string to encrypt" | Get-EncryptedAnsibleVault
Write-Output -InputObject "vault_variable: !vault |`n    $($vault_text.Replace("`n", "`n    "))"

# add a new variable to an existing vault file
$vault_pass = Read-Host -Prompt "Enter vault password" -AsSecureString
$vault_text = Get-DecryptedAnsibleVault -Path C:\temp\vault.yml -Password $vault_pass
$vault_text += "`nanother_host_secret: you'll never guess this"
Get-EncryptedAnsibleVault -Value $vault_text -Password $vault_pass | Set-Content -Path C:\temp\vault.yml

AnsibleVault in Action, the sky is the limit with what you can do

References

I would like to point out a few different blogs/sites that helped me along the way

  • PBKDF2 SHA256 C# Implementation
  • Lee Holmes and P/Invoke through .NET Reflection
  • The Scripting Guys – Another P/Invoke through .NET Reflection
  • Hans Wolff and his AES CTR implementation
  • Rambling Cookie Monster and his PSake reference

Обновлено:
Опубликовано:

Используемые термины: Ansible.

Инструкция представляет из себя шпаргалку по работе с Ansible. У автора не стоит задачи подробного пояснения всех операций — только описание задачи и пример того, как ее можно решить с помощью ansible. Для более подробного описания я постараюсь указать ссылки на официальную документацию. Также в данную шпаргалку не войдут все возможные действия, которые можно выполнить с помощью данной системы — только популярные и те, с которыми приходилось сталкиваться самому автору. По мере возможности, их список будет пополняться.

Для удобства, мы попробуем разбить примеры на операции, которые логически можно объединить в одну группу.

Получение информации
Работа с переменными
Проверки и условия
Работа с репозиториями
Установки пакетов, модулей и расширений
Настройка системы
Работа с системами безопасности
Работа с папками и файлами
Содержимое файлов
Управление сертификатами
Виртуализация VMware
Виртуализация Proxmox
Работа с Docker
Облачная платформа OpenStack
Работа с базами данных
Запуск плейбука
Обработка ошибок
Работа с выводом и строками
Вставки ролей, задач и плейбуков
Работа с шаблонами Jinja2
Meta
Разное
Читайте также

Получение информации

Сюда войдут примеры, которые позволят собирать информацию, выводить ее на экран, помогать в отладке и всякое такое.

1. Сбор общей информации о системе.

Чтобы собрать информацию о системе, на которой выполняется сценарий ansible, нужно использовать модуль setup, например:

— name: Get service facts
  setup:

Или можно применить фильтр, чтобы сбор выполнялся быстрее:

— name: Get service facts
  setup:
    filter: ‘ansible_os_family’

Данная информация будет записана в переменную ansible_facts. При желании, ее значение можно отобразить с помощью модуля debug:

— name: Print all available facts
  debug:
    var: ansible_facts

Также мы можем обратиться к конкретному элементу массива ansible_facts, получив информацию о конкретной настройке или опции:

    …
    ansible_facts.hostname

О setup: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/setup_module.html.

2. Получить определенную информацию о компьютере.

Выполняется с помощью различных модулей с окончанием _facts. Рассмотрим примеры.

а) список сервисов. Для этого существует service_facts:

— name: Populate service facts
  ansible.builtin.service_facts:

— name: Print all available services
  debug:
    var: ansible_facts.services

* цель достигается двумя задачами. В первой мы собираем информацию о сервисах с помощью service_facts, второй — выводим на экран содержимое.

О service_facts: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/service_facts_module.html.

б) установленные пакеты. Используем package_facts:

— name: Gather the package facts
  package_facts:
    manager: auto

— name: Print all available packages
  debug:
    var: ansible_facts.packages

О package_facts: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/package_facts_module.html.

3. Отображение на экран переменной.

Выше мы уже использовали debug для отображения переменной — принцип тот же:

— name: Show Value of Variable
  debug:
    msg: «{{ variable }}»

* при выполнении задачи на экране мы увидим значение переменной variable. Обратите внимание, что запись ansible.builtin.debug и debug — это одно и то же, то есть, ansible.builtin можно не писать.

О debug: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/debug_module.html.

4. Сохранение результата при выполнении задания.

Создать и использовать переменную можно в процессе выполнения работы с помощью директивы register:

— name: Run a shell command and register its output as a variable
  shell: command
  register: command_result

Также нам может понадобиться сохранить в переменную результат обработки переменной:

— name: Set variable
  set_fact:
    my_hosts: «{{ command_result.stdout }}»

* в данном случае мы взяли переменную, которую получили в примере выше и разбили строку по знаку «:». Таким образом, мы получим массив данных, который будет сохранен в переменную my_hosts.

Работа с переменными

В сценариях нам постоянно нужно будет использовать переменные. Их можно передавать при запуске, получать в процессе выполнения задач или описать в самом сценарии заранее. Рассмотрим эти действия на некоторых примерах.

1. Переменные в ролях.

Для описания переменных в ролях существуют два каталога — vars и defaults. Последние имеют самый низкий приоритет, это значит, что их можно переопределить.

Независимо от каталога, в котором идет описание переменной, синтаксис один:

hostname: mail.dmosk.local

* в данном примере определена переменная hostname со значением mail.dmosk.local.

2. Переменные в файле inventory.

Инвентарный файл используется для описания хостов, которые могут быть задействованы при работе сценариев ansible. Переменная может быть определена на уровне всех хостов или на уровне конкретного хоста:

all:
  vars:
    tower_user_name: master
  hosts:
    192.168.0.10:
      human_name: mail
    192.168.0.11:
      human_name: www

* в нашем примере будут определены 3 переменные:

  • tower_user_name для всех машин.
  • human_name для 192.168.0.10.
  • human_name для 192.168.0.11.

3. Переменные в плейбуке.

Задаются в файле плейбука:

— hosts: all
  …
  vars:
    vm_name: test-vm

* в данном примере это переменная vm_name.

4. При запуске плейбука.

Когда мы запускаем на выполнение наш плейбук, мы можем передать переменные с помощью опции extra-vars:

ansible-playbook … —extra-vars «{ ‘address’:’dmosk.local’, ‘version’:’5.1.2′ }»

* передаем переменные address и version.

Также extra-vars принимает в качестве значений файл, в котором будут переменные:

ansible-playbook … —extra-vars «@secrets_file.yml»

5. Системные переменные.

Мы можем использовать не только определенные нами переменные, но и вытаскивать системные переменные. Для этого используется плагин lookup вместе с модулем env:

— name: Show Value of System Variable
  debug:
    var: lookup(‘env’, ‘HOME’)

* в данном примере мы получим значение системной переменной HOME (домашней директории пользователя, под которым запущен сценарий ansible).

Или можно записать ее в переменную, чтобы использовать в других задачах:

— name: Set sys_home From System Variable
  set_fact:
    sys_home: «{{ lookup(‘env’, ‘HOME’) }}»

— name: Show Value of sys_home Variable
  debug:
    var: sys_home

О lookup + env: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/env_lookup.html.

Проверки, условия и действия на основе этих проверок

В данную группу войдут действия, которые помогут нам ограничить выполнение задач или изменить ход выполнения.

1. Проверка на пустую папку.

Задачи сводится к двум операциям:

  • получении списка файлов в целевом каталоге (например, с помощью команды ls) и регистрации полученного значения в переменную с помощью register.
  • проверка содержимого переменной, которую мы получили на шаге 1 с помощью when.

Пример будет таким:

— name: Register Contents of PGDATA Folder
  shell: ls /var/lib/postgresql/11/main
  register: pg_contents

— name: Init PostgreSQL DB
  shell: /opt/pgpro/std-11/bin/pg-setup initdb
  environment:
    PGDATA: «/var/lib/postgresql/11/main»
  when: pg_contents[«stdout_lines»] | length == 0

* в данном примере мы в первой задаче выводим содержимое каталога /var/lib/postgresql/11/main и помещаем его в переменную pg_contents. Во второй задаче мы уже проверяем с помощью when количество строк — если их 0, то тогда выполняем команду initdb. На практике, это важно, так как инициализация базы PostgreSQL при непустом каталоге выводит сообщение об ошибке.

О when: https://docs.ansible.com/ansible/latest/user_guide/playbooks_conditionals.html.

2. Проверить, определена ли переменная.

Для этого используется опция is defined (определена) или is not defined (не определена):

when: pgpro is defined

when: pgpro is not defined

* в данном примере мы проверим наличие переменной pgpro. На практике такая проверка имеет значение, так как если мы попробуем выполнить действия с несуществующей переменной, Ansible нам вернет ошибку.

В официальной документации про это сказано в статье о when (ссылка выше).

3. Выполнение команды, если сервис в рабочем состоянии.

Нам необходимо получить информацию о службах с помощью service_facts, после чего можно уже делать проверку с помощью when:

— name: Populate service facts
  ansible.builtin.service_facts:

— name: Stop Service If Running One
  shell: systemctl stop apache2
  when: ansible_facts.services[«apache2.service»].state | default(false) == «running»

* в данном примере мы проверим, есть ли служба apache2 и запущена ли она. Если это так, то мы ее останавливаем.

Подробнее о service_facts можно прочитать в документации (ссылка выше в разделе 4. Получить список сервисов).

4. Существует ли файл.

Проверка может быть выполнена с помощью stat:

— name: Register File Stat
  stat:
    path: /etc/nginx/nginx.conf
  register: stat_result

— name: Cat file if exists
  shell: cat /etc/nginx/nginx.conf
  when: stat_result.stat.exists

* в данном примере будет выполнено чтение файла /etc/nginx/nginx.conf, если он существует.

О stat: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/stat_module.html.

5. Операционная система.

С помощью переменных ansible_os_family и ansible_distribution_major_version мы можем получить информацию об операционной системы, которая работает на целевом компьютере. Это можно использовать для выполнения проверок и задания условий.

а) Проверка по семейству:

— name: Run task1 if OS is Debian
  …
  when: ansible_os_family == «Debian»

— name: Run task2 if OS is Red Hat
  …
  when: ansible_os_family == «RedHat»

* в данном примере мы запустим задачу task1 в системах на основе Debian и task2 — на основе RedHat.

б) Проверка по версии системы:

— name: Run task1 if OS is Debian 9
  …
  when: ansible_os_family == «Debian» and ansible_distribution_major_version == «9»

— name: Run task2 if OS is Red Hat 8
  …
  when: ansible_os_family == «RedHat» and ansible_distribution_major_version == «8»

— name: Run task3 if OS is Red Hat less 8
  …
  when: ansible_os_family == «RedHat» and ansible_distribution_major_version | int < 8

* в данном примере мы запустим задачу task1 в системах на основе Debian версии 9, task2 — на основе RedHat версии 8 и task3 на системах ниже 8 версии.

6. Выдать ошибку и остановить выполнение.

Для этого предусмотрен модуль fail или опция задания failed_when. Рассмотрим примеры использования обоих.

а) Модуль fail. Является отдельным заданием. Его можно выполнить просто так, но без условия его применение нелогично. Пример:

— name: Stop the executing if variable is empty
  fail:
    msg: The variable srv_ip is not defined or its empty.
  when: srv_ip is not defined or srv_ip == «»

* в нашем примере, если переменная srv_ip окажется пустой или неопределенной, мы получим ошибку с сообщением, определенным в опции msg.

О fail: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/fail_module.html.

б) Опция failed_when. Задает условие, при соблюдении которого задание выполняется с ошибкой, например:

— name: Fail task when the command error output prints FAILED
  ansible.builtin.command: /usr/bin/example-command -x -y -z
  register: command_result
  failed_when: «‘FAILED’ in command_result.stderr»

* данный пример с официального сайта ansible (ссылка ниже). Задание выдаст ошибку, если в результате выполнения скрипта /usr/bin/example-command будет текст FAILED.

О failed_when: https://docs.ansible.com/ansible/latest/user_guide/playbooks_error_handling.html.

7. Ожидания с помощью wait_for.

Мы можем создать задание, которое остановит выполнение плейбука, пока не будет выполнено определенное условие:

  • Просто ждать определенное время.
  • Удачное соединение по сети с узлом.
  • Определенное содержимое в файле.
  • Пока не появится или удалится файл или папка.

О wait_for https://docs.ansible.com/ansible/latest/collections/ansible/builtin/wait_for_module.html.

Рассмотрим несколько примеров.

а) Ожидаение файла:

— name: Wait rpm file
  wait_for:
    path: /tmp/build/my_package.rpm

* в данном примере выполнение плейбука остановится, пока по пути /tmp/build/my_package.rpm не появится файл.

б) Ждем, пока хост не станет доступен по SSH:

— name: Ждем, пока не сможем подключиться к хосту по SSH
  wait_for:
    host: 192.168.1.11
    port: 22
    state: «started»
    timeout: 180

8. Проверка условий с выводом ошибки или успеха.

Предположим, нам нужно сделать какую-либо проверку, в результате которой ansible должен вернуть ошибку или продолжить выполнение. Для этого хорошо подходит модуль assert. Рассмотрим несколько примеров.

а) Значение переменной my_key должно быть от 0 до 100:

— name: Значение переменной my_key должно быть от 0 до 100
  assert:
    that:
      — my_key <= 100
      — my_key >= 0
    fail_msg: «Неправильное значение my_key — должно быть от 0 до 100»
    success_msg: «Значение my_key верное»

б) Значение my_login не должно быть только из цифр:

  — name: Проверяем название датасета
    assert:
      that:
        — my_login is not regex(«^[0-9]+$»)
      fail_msg: «Имя my_login не может состоять только из цифр»
      success_msg: «Имя my_login задано верно»

О assert: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/assert_module.html.

Работа с репозиториями

Рассмотрим настройку репозиториев для систем на базе DEB и RPM.

Работа с репозиториями RPM

Ведется с помощью модуля yum_repository.

О yum_repository: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/yum_repository_module.html.

1. Добавление репозитория:

— name: «Add YUM repository»
  yum_repository:
    name: new_yum_repo
    description: «New Yum Repo Description»
    file: new_repo_file
    baseurl: https://download.fedoraproject.org/pub/epel/$releasever/$basearch/
    enabled: yes
    gpgcheck: no
    validate_certs: no

2. Для удаления репозитория также используем yum_repository

— name: «Remove YUM repositories»
  yum_repository:
    name: pgsql95
    state: absent

* в нашем примере будет удален репозиторий с названием pgsql95.

Ansible может не распозновать некоторые репозитории, которые настроена в пользовательских файлах. В таком случае необходимо указать на эти файлы с помощью опции file:

— name: «Remove YUM repositories»
  yum_repository:
    name: pgsql95
    state: absent
    file: pgsql

3. Установка пакетов из dnf-модулей. Начиная с версии 8 дистрибутивов на основе RPM пакеты могут устанавливаться из модульных репозиториев, в которых идет четкое разграничение между версиями пакетов. В ansible, если необходимо установить пакет из определнного модуля, используем сценарий на подобие:

— name: 
  dnf:
    name:
      — @postgresql:13/client
      — @postgresql:13/server
    state: present

* в данном примере мы установим пакеты postgresql-client и postgresql-server из модульного репозитория postgresql:13.

Работа с репозиториями DEB

1. Добавление репозитория:

По ссылке:

— name: «Add DEB repository»
  apt_repository:
    repo: deb http://dl.google.com/linux/chrome/deb/ stable main
    filename: new_deb_repo

С использованием персонального архива пакетов (PPA):

— name: «Add DEB PPA»
  apt_repository:
    repo: ppa:deadsnakes/ppa

О apt_repository: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/apt_repository_module.html.

2. Для удаления репозитория нужно воспользоваться директивой state со значением absent, например:

— name: «Remove Debian default repository»
  apt_repository:
    repo: deb https://ftp.debian.org/debian stretch main contrib
    state: absent

* в данном примере будет удален репозиторий, который идет по умолчанию для Debian Stretch.

3. Импорт ключа.

Рассмотрим примеры с добавлением gpg-ключа по URL и с сервера ключей:

  — name: Import postgresql repo key
    apt_key:
      url: https://www.postgresql.org/media/keys/ACCC4CF8.asc
      state: present

* импорт ключа из файла https://www.postgresql.org/media/keys/ACCC4CF8.asc.

  — name: Import keyserver.ubuntu.com repo keys
    apt_key:
      keyserver: keyserver.ubuntu.com
      id: 648ACFD622F3D138
      state: present

* импорт ключа с идентификатором 648ACFD622F3D138 из сервера ключей keyserver.ubuntu.com.

О apt_key: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/apt_key_module.html.

4. Обновление кэша репозитория.

Выполняется с помощью модуля apt и опции update_cache:

— name: Update repositories cache
  apt:
    update_cache: yes

Об apt: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/apt_module.html.

Установки пакетов, модулей и расширений

В данном разделе мы коснемся всего, что приводит к установке чего бы то ни было. А именно:

  • Установки пакетов в систему.
  • Загрузки исходников.
  • Установке дополнительных модулей.
  • Распаковке архивов.

Обратите внимание на опцию state. С ее помощью мы можем управлять поведением установки:

  • present — пакет должен быть установлен.
  • latest — должна быть установлена последняя версия.
  • absent — пакет должен быть удален.

Рассмотрим это подробнее.

1. Установка/удаление пакетов в систему.

Выполняется с помощью универсального модуля package или с помощью специализированных yum и apt. Последние имеют больше полезных опций.

а) package:

— name: Install NTP-client
  package:
    name: chrony
    state: latest

* по данной инструкции в нашей системе должен быть установлен пакет chrony последней версии.

Для установки пакета конкретной версии, ее нужно указать в названии пакета:

— name: Install NTP-client
  package:
    name: chrony-3.4
    state: present

О package: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/package_module.html.

б) dnf/yum.

Установка:

— name: Install NTP-client
  yum:
    name: chrony
    state: present

Для удаления:

— name: Remove NTP-client
  yum:
    name: chrony
    state: absent

Обновление:

— name: Обновить пакеты
  yum:
    name: ‘*’
    state: latest

А так можно сделать даунгрейд:

— name: Downgrade NTP-client
  yum:
    name: chrony-1.0.0
    state: present
    allow_downgrade: true

О yum: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/yum_module.html.

в) apt.

Установка:

— name: Install NTP-client
  apt:
    name: chrony
    state: present

Если нам нужно установить пакет из файла deb, синтаксис будет следующий:

— name: «Install lib for postgresql»
  apt:
    deb: http://ftp.ru.debian.org/debian/pool/main/l/llvm-toolchain-7/libllvm7_7.0.1-8+deb10u2_amd64.deb
    state: present

* в данном примере мы устанавливаем пакет libllvm7 из файла deb, который доступен по url http://ftp.ru.debian.org/debian/pool/main/l/llvm-toolchain-7/libllvm7_7.0.1-8+deb10u2_amd64.deb.

Выполнить только обновление установленных пакетов:

— name: Установка обновлений
  apt:
    name: «*»
    state: latest
    update_cache: true
    cache_valid_time: 3600

Об apt: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/apt_module.html.

2. Установка модуля в nodejs.

Установка модулей в nodejs выполняется с помощью npm. Для него в ansible есть отдельная функция:

— name: Install nodejs modules.
  npm:
    name: newman
    global: yes

* в данном примере будет выполнена установка newman, которая будет доступна всем проектам (опция global).

О nodejs npm: https://docs.ansible.com/ansible/latest/collections/community/general/npm_module.html.

3. Установка расширений для python.

Используем модуль pip. Рассмотрим несколько примеров.

а) установка пакета python:

— name: Pip install psycopg2
  pip:
    name: psycopg2
    state: present

* в данном примере будет установлен psycopg2.

б) обновление пакетов:

— name: Upgrade pip and wheel
  pip:
    name: «{{ item }}»
    extra_args: —upgrade
    executable: pip3
  loop:
    — pip
    — wheel

* в нашем примере будут обновлены пакеты pip и wheel.

в) использовать определенную версию pip:

— name: Install python modules with pip3
  pip:
    name: patroni[consul]
    executable: pip3
    state: present

О pip: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/pip_module.html.

4. Распаковка архива.

Выполняется с помощью unarchive:

— name: Unpacking Nginx Source
  unarchive:
    src: «http://nginx.org/download/nginx-{{ nginx_ver }}.tar.gz»
    dest: /tmp/
    remote_src: yes
    creates: /tmp/nginx-{{ nginx_ver }}.tar.gz

* в данном примере мы распакуем исходник для nginx в каталог /tmp. Обратите внимание на две вещи:

  • Мы используем переменную nginx_ver. Данная переменная должна быть определена при запуске плейбука, или в инвентарном файле, или в var, или в default. Подробнее в соответствующем разделе выше.
  • Опция creates позволит не выполнять операцию, если существует файл /tmp/nginx-{{ nginx_ver }}.tar.gz.

Об unarchive: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/unarchive_module.html.

Настройка системы

В данном разделе мы рассмотрим процессы, которые больше подходят для категории настройки системы. 

1. Добавить задание в cron.

Выполняется с помощью модуля cron:

— name: Add Job for Run Command
  cron:
    name: Start Script
    job: «/scripts/command.sh»
    user: root
    minute: «0»
    hour: «*/6»
    day: «*»
    month: «*»
    weekday: «*»

* в данном примере мы создадим задание для запуска команды /scripts/command.sh каждый день, каждые 6 часов.

О cron: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/cron_module.html.

2. Создание учетной записи.

Для этого есть модуль user. У него много опций, рассмотрим некоторые из них.

а) Простая учетная запись:

— name: Create User1
  user:
    name: user1
    shell: /bin/bash
    create_home: yes

* в данном примере мы создадим пользователя user1 с домашней директорией. Также мы указали для использования командную оболочку /bin/bash.

б) Для создания системной учетной записи нам достаточно:

— name: Create User Consul
  user:
    name: consul
    system: yes
    comment: «Consul Agent»

* в данном примере будет создана учетная запись consul.

в) Создаем пользователя с паролем:

— name: Create User2
  user:
    name: user2
    shell: /bin/bash
    create_home: yes
    password: «{{ ‘my_passw0rd’ | password_hash(‘sha512’) }}»

* будет создан пользователь user2 с паролем my_passw0rd.

г) Добавляем пользователя в группу:

— name: Добавляем пользователя clamav в группу amavis
  user:
    name: vmail
    groups: mail
    append: yes

* в данном примере пользователь vmail будет добавлен в группу mail.

О user: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/user_module.html.

д) Создание группы:

— name: Создаем группу vmail
  group:
    name: vmail
    state: present

О group: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/group_module.html.

3. Работа с systemd.

Для данной настройки есть одноименный модуль systemd. Рассмотрим варианты его использования.

а) перечитать конфигурацию (необходимо делать каждый раз, когда мы меняем настройки юнита):

— name: systemd reload
  systemd:
    daemon_reload: yes

б) разрешить сервис (автозапуск):

— name: mysql enable
  systemd:
    name: mysql
    enabled: yes

* для сервиса mysql.

в) перезапустить сервис:

— name: mysql reload
  systemd:
    name: mysql
    state: restarted

г) остановить сервис:

— name: mysql stoped
  systemd:
    name: mysql
    state: stopped

О systemd: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/systemd_module.html.

4. Имя компьютера.

Для указания имени компьютера можно использовать модуль hostname:

— name: Задаем имя компьютера
  hostname:
    name: myweb
    use: systemd

* в данном примере мы задаем имя myweb. Обратите внимание на опцию use — в зависимости от операционной системы или ее версии, ее значение должно отличаться.

О hostname: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/hostname_module.html.

5. Часовой пояс.

Часовой пояс можно настроить с помощью модуля timezone, например:

— name: Задаем часовой пояс
  timezone:
    name: Europe/Moscow

О timezone: https://docs.ansible.com/ansible/latest/collections/community/general/timezone_module.html.

6. Локали.

Добавить локаль можно с помощью модуля locale_gen:

— name: Добавить локаль в систему
  locale_gen:
    name: 
      — en_US.UTF-8
      — ru_RU.UTF-8
    state: present

О locale_gen: https://docs.ansible.com/ansible/latest/collections/community/general/locale_gen_module.html.

Локаль по умолчанию назначается с помощью shell:

— name: Задаем локаль по умолчанию
  command: localectl set-locale LANG=en_US.UTF-8

Безопасность

Вынесем в отдельный раздел задачи, связанные с системами безопасности.

1. Настройка брандмауэра.

Выполняется разными модулями в зависимости от используемой системы управления netfilter:

  • firewalld
  • iptables
  • ufw

Рассмотрим небольшие примеры.

а) firewalld:

— name: permit traffic in default zone for https service
  firewalld:
    service: https
    permanent: yes
    state: enabled

Подробнее: https://docs.ansible.com/ansible/latest/collections/ansible/posix/firewalld_module.html.

б) iptables.

Запрещаем:

— name: Заблокировать запросы от определенного IP адреса
  iptables:
    chain: INPUT
    source: 8.8.8.8
    jump: DROP

Разрешаем: 

— name: Добавляем разрешающее правило для портов
  iptables:
    action: insert
    rule_num: 1
    chain: INPUT
    protocol: tcp
    destination_port: «{{ item }}»
    ctstate: NEW
    jump: ACCEPT
  loop:
    — «80»
    — «443»

Подробнее: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/iptables_module.html.

в) UFW.

Добавить 80 порт:

— name: Allow all access to tcp port 80
  ufw:
    rule: allow
    port: ’80’
    proto: tcp

Добавить порты с циклом:

— name: Allow Ports in Firewall
  ufw:
    rule: allow
    port: «{{ item.port }}»
    proto: «{{ item.proto }}»
    comment: «{{ item.comment }}»
  loop:
    — { port: 5432, proto: tcp, comment: ‘PostgreSQL’ }

Подробнее: https://docs.ansible.com/ansible/latest/collections/community/general/ufw_module.html.

2. SELinux.

Смена контекста безопасности для файлов:

— name: Разрешить контекст для PostgreSQL
  sefcontext:
    target: ‘/data/pgsql(/.*)?’
    setype: postgresql_db_t
    state: present

Для применения политики придется воспользоваться шелом:

— name: Применяем контекст для каталога
  command: restorecon -irv /data/pgsql

Работа с папками и файлами

Рассмотрим задачи, которые помогут нам создавать, копировать и работать с файлами.

1. Создание каталогов и файлов.

Создание файлов и каталогов выполняется с помощью модуля file.

а) для каталога в качестве state указываем directory:

— name: Create Directories
  file:
    path: «{{ item }}»
    state: directory
    owner: www-data
    group: www-data
    mode: 0755
  loop:
    — ‘/var/www/site1’
    — ‘/var/www/site2’

* в данном примере мы создадим 2 каталога: site1 и site2 в каталоге /var/www.

б) для создания файла убираем опцию state (или даем ей значение touch):

— name: Create File
  file:
    path: «/var/www/site1/index.php»
    state: touch
    owner: www-data
    group: www-data
    mode: 0644

* в данном примере мы созданим файл index.php в каталоге /var/www/site1.

в) для создания симлинка используем state со значением link:

— name: Create a symbolic link from foo to bar
  file:
    src: /usr/bin/foo
    dest: /usr/sbin/bar
    state: link

О file: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/file_module.html.

2. Задать права.

Это можно выполнить с помощью модуля создания файла или каталога:

— name: Set File Rights
  file:
    path: «/var/www/site1/index.php»
    owner: www-data
    group: www-data
    mode: 0664

* обратите внимание, это пример из предыдущено раздела. Для созданного файла мы просто немного изменили права.

3. Копирование файлов из каталога.

Для копирования данных мы используем модуль copy:

— name: Copy Cert File If Different
  copy:
    src: «{{ item }}»
    dest: /etc/ssl/dmosk
    remote_src: no
    mode: 0644
    owner: root
    group: root
  with_fileglob:
    — files/*

* в данном примере мы прочитаем все содержимое каталога files на компьютере с ansible, и скопируем его в каталог /etc/ssl/dmosk на целевом компьютере.

О copy: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/copy_module.html.

4. Загрузка файла на хост ansible.

Реализуется с помощью fetch и является обратной операцией copy. То есть, мы загрузим файл с хоста, где выполняется задача на хост, с которого запускается ansible.

— name: Загрузка файла на ansible хост
  fetch:
    src: /tmp/newcert
    dest: /tmp/
    flat: yes

Подробнее о fetch: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/fetch_module.html.

5. Используем шаблон.

Копирование из шаблона отличается от копирования из файла тем, что в шаблоне могут использоваться переменные, которые будет заменяться их значениями в момент копирования. Для самого процесса копирования из шаблона используется модуль template:

— name: Create Config for Consul Agent
  template:
    src: templates/consul/config.json.j2
    dest: /etc/consul.d/config.json

* в данном примере мы возьмом шаблон templates/consul/config.json.j2 на компьютере ansible и разместим его в по пути /etc/consul.d/config.json на целевом компьютере.

Мы можем вывести в консоль результат обработки шаблона следующим образом:

— name: Show Templating Results
  debug:
    msg: «{{ lookup(‘template’, ‘./config.json.j2’) }}»

О template: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/template_module.html.

6. Архивирование.

Создать архив из файла или каталога можно с помощью модуля archive:

— name: «Use gzip to compress folder»
  archive:
    path: /etc/raddb
    dest: «/tmp/raddb.gz»
    format: gz

* в данном примере мы создадим архив из каталога /etc/raddb и сохраним его в файл /tmp/raddb.gz.

О archive: https://docs.ansible.com/ansible/latest/collections/community/general/archive_module.html.

Для распаковки архивов используется модуль unarchive, о котором мы говорили выше.

7. Поиск файлов и папок.

Выполняется с помощью модуля find. Особый интерес представляет в контексте поиска файлов и выполнение над ними определенных действий. Разберем несколько примеров.

а) Удалить последние 30 файлов. Задача решается в два этапа:

  • ищем содержимое целевого каталога.
  • сотритуем список найденных по времени изменения файлов и удаляем все, что идут после определенного числа объектов.

Поиск выполняем с помощью модуля find, удаление — file:

— name: «Get list of backup files»
  find:
    paths: «/backup»
    file_type: file
  register: founds

— name: «Delete last 30 Copies»
  file:
    path: «{{ item }}»
    state: absent
  loop: «{{ (founds.files | sort(attribute=’mtime’, reverse=True) | map(attribute=’path’) | list )[30:] }}»

* в данном примере мы ищем файлы в каталоге /backup, после чего сортируем найденное и удаляем по списку все файлы, которые идут после 30-го.

б) Удаление архивов для логов. Также выполняем в два этапа — более сложный поиск и удаление с помощью file:

— name: «Get a list of logs to be deleted»
  find:
    paths: «/var/log»
    file_type: file
    patterns: ‘*.gz,*.log-*,*.old,*.[0-9].log,*.log.[0-9],*-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]’
    recurse: yes
  register: logs_to_delete

  
— name: «Delete logs»
  file:
    path: «{{ item }}»
    state: absent
  loop: «{{ logs_to_delete.files | map(attribute=’path’) | list }}»

* в данном примере мы применили регулярные выражения для поиска различных вариантов логов, которые являются архивными. После мы сохраняем результат поиска в переменную logs_to_delete, которую используем для получения списка путей до найденных файлов.

О find: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/find_module.html.

8. Скачать файл с помощью curl.

Для этого используется модуль uri. Простой пример:

— name: CURL simple download file
  uri:
    url: https://www.dmosk.ru/files/winsetupfromusb.zip
    dest: /tmp

* в данном примере мы загрузим файл https://www.dmosk.ru/files/winsetupfromusb.zip в каталог /tmp.

Пример посложнее:

— name: CURL download file with token auth
  uri:
    url: https://gitlab.dmosk.ru/api/v4/projects/555/repository/files/folder%2Fpath%2Fdata.sql/raw?ref=master
    dest: /tmp/data.sql
    owner: dmosk
    group: dmosk
    mode: 640
    headers:
      PRIVATE-TOKEN: access-token

* в данном примере мы скачаем файл с ресурса, где требуется аутентификация по токену, который передается в заголовке. Заголовки мы передаем с помощью параметра headers. Также мы задаем права на загруженный файл и делаем в качестве владельца пользователя и группу dmosk.

Об uri: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/uri_module.html.

9. Создание временных файлов и папок.

Иногда, для работы нужно временное хранилище файлов, которое после работы можно будет удалить. Для работы с данным хранилишем в ansible можно использовать модуль tempfile.

Пример создания каталога:

— name: Create temporary ansible directory
  tempfile:
    state: directory
    suffix: ansible
  register: tmp

Путь до созданного каталога будет записан в переменную tmp.path.

После не забываем удалить созданную папку:

— name: Remove temporary ansible directory
  file:
    path: «{{ tmp.path }}»
    state: absent
  when: tmp.path is defined

О tempfile: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/tempfile_module.html.

10. Работа с GIT.

О Git: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/git_module.html.

а) Клонирование проекта из гита.

Выполняется с помощью модуля git.

— name: Clone docker-compose
  git:
    repo: «https://github.com/docker/compose.git»
    dest: /tmp/docker-compose

* в данном примере мы сделаем клон репозитория в каталог /tmp/docker-compose.

б) Изменение содержимого для файла.

Модуль ansible не поддерживает отпавку изменений в git, но можно воспользоваться API. Например, для gitlab обновить контент файла можно с помощью модуля uri:

— name: 
  uri:
    url: «https://gitlab.dmosk.ru/api/v4/projects/666/repository/files/folder%2Ffilename»
    method: PUT
    return_content: false
    body_format: json
    body:
        branch: «main»
        author_email: «master@dmosk.ru»
        author_name: «Dmitriy Mosk»
        content: «Text for file»
        commit_message: «update filename»
    headers:
      PRIVATE-TOKEN: 00000_1111111_33333
      Content-Type: application/json
    validate_certs: no

* где:

  • url — полный путь до файла. Обратите внимание на:
    • gitlab.dmosk.ru — адрес нашего сервера gitlab.
    • 666 — идентификатор проекта. Его можно посмотреть в настройках самого проекта.
    • folder%2Ffilename — путь до файла. В нормальном формате, это folder/filename.
  • body — содержит данные, которые будут отправлены на сервер для смены контента.
  • headers PRIVATE-TOKEN — токен доступа к API. Его можно создать в настройках профиля учетной записи Gitlab.

Подробнее о работе API в гиблабе: https://docs.gitlab.com/ee/api/repository_files.html.

Содержимое файла

С помощью Ansible мы можем менять содержимое строк как в файлах — вставлять, удалять и редактировать, так и полученных результатах. Рассмотрим несколько примеров.

1. Просмотр содержимого файла.

Содержимое файлов на удаленном сервере и на стороне ansible просматривается по-разному. Рассмотрим оба варианта.

а) На ansible.

Чтобы увидеть содержимое файла, который лежит на стороне ansible, используем плагин lookup:

— name: Show File Content
  debug:
    msg: «{{ lookup(‘file’, ‘/etc/ntp.conf’) }}»

* в данном примере мы просто покажем содержимое файла /etc/ntp.conf.

О lookup: https://docs.ansible.com/ansible/latest/plugins/lookup.html.

б) На удаленном сервере.

Выполняется в два этапа — просмотр содержимого с сохранением результата в переменную, и вывод содержимого переменной:

— name: Read file content
  shell: cat /path/to/file
  register: file_content

— name: Show file content
  debug:
    var: file_content.stdout

2. Замены строки в файлах.

Замены выполняются с помощью модуля replace. Рассмотрим несколько примеров.

а) Простой поиск и замена строки:

— name: Check configs (comment server address 127.0.0.1)
  replace:
    path: «/etc/nginx/nginx.conf»
    regexp: ‘^server.address=127.0.0.1$’
    replace: ‘#server.address=127.0.0.1’

* в данном примере мы добавляем комментарий к строке server.address=127.0.0.1.

б) Замена с подстановкой найденного содержимого:

— name: Check configs (comment server address 127.0.0.1)
  replace:
    path: «/etc/nginx/nginx.conf»
    regexp: ‘^server.address=(.*)$’
    replace: ‘# commented for \1’

* в данном примере мы находим строку server.address с любым значением и меняем ее на строку, в которой будет прописано это значение.

О replace: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/replace_module.html.

3. Добавление и удаление строк в файле.

Мы можем менять содержимое файла с помощью модуля lineinfile. Рассмотрим некоторые примеры работы с ним.

а) Удалить строку:

— name: Remove server strings
  lineinfile:
    path: «/etc/chrony.conf»
    regexp: ‘^server .*’
    state: absent

* в данном примере мы удалим все строки из файла /etc/chrony.conf, которые начинаются на server.

б) Добавить строку:

— name: Add server strings
  lineinfile:
    path: «/etc/chrony.conf»
    line: ‘server ntp.server.local’

* в данном примере мы добавим строку server ntp.server.local в файл /etc/chrony.conf. Если данная запись уже есть в файле, ansible ничего не станет менять.

в) Добавить строку с использованием регулярного выражения:

— name: Ensure SELinux is set to enforcing mode
  lineinfile:
    path: /etc/selinux/config
    regexp: ‘^SELINUX=’
    line: SELINUX=enforcing

* пример взят с официального сайта. В данном случае мы гарантируем наличие строки SELINUX=enforcing — либо для директивы SELINUX будет задано определенное значение, либо строка будет полностью вставлена в конфигурационный файл /etc/selinux/config.

О lineinfile: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/lineinfile_module.html.

4. Разбить строку.

Выполняется с помощью метода split. Рассмотрим пример, когда мы регистрируем результат при выполнении команды, после чего мы разобьем его по предложениям:

— name: Run a shell command and register its output as a variable
  shell: command
  register: command_result

— name: Parse string for dot and show results
  debug:
    var: command_result.stdout.split(«.»)

* в данном примере мы получим массив данных из предложений.

Еще один пример — разбить FQDN имя на короткое и домен. Это можно сделать с применением данных конструкций:

— name: Получаем две переменные с коротким именем и именем домена
  set_fact:
    short_name: «{{ fqdn_name.split(‘.’)[0] }}»
    domain_name: «{{ fqdn_name.split(‘.’)[1:] | join(‘.’) }}»

5. Добавить блок текста.

Можно сделать с помощью модуля blockinfile:

— name: Simple config for PDF converter
  blockinfile:
    path: /opt/app/conf/app.yml
    block: |
      # Simple config
      option:
          key1: value1
          key2: value2
          key3: value3

О blockinfile: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/blockinfile_module.html.

6. Сохранить содержимое переменной в файл.

Выше мы рассматривали модуль copy для копирования файлов. Мы также можем его использовать для помещения содержимого переменной в файл:

— name: Save my_variable to data.txt file
  copy:
    content: «{{ my_variable }}»
    dest: ‘/tmp/data.txt’

* в данном примере содержимое переменной my_variable будет сохранено в файле /tmp/data.txt.

Работа с SSL

В данном разделе рассмотрим задачи управления сертификатами и шифрованием при передачи данных.

1. Добавить публичный ключ хоста в known_hosts.

Делается с помощью known_hosts. Пример из официальной документации:

— name: Tell the host about our servers it might want to ssh to
  known_hosts:
    path: /etc/ssh/ssh_known_hosts
    name: foo.com.invalid
    key: «{{ lookup(‘file’, ‘pubkeys/foo.com.invalid’) }}»

* в данном примере мы добавим ключ из файла pubkeys/foo.com.invalid в /etc/ssh/ssh_known_hosts.

О known_hosts: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/known_hosts_module.html.

2. Создание новых SSH-ключей для сервера.

Создание ключей реализуется с помощью модуля openssh_keypair:

— name: Generate New SSH Host Keys
  openssh_keypair:
    path: «/etc/ssh/ssh_host_{{ item.type }}_key»
    owner: root
    state: present
    type: «{{ item.type }}»
    size: «{{ item.size }}»
    force: yes
  loop:
    — { type: dsa, size: 1024 }
    — { type: ecdsa, size: 521 }
    — { type: ed25519, size: 2048 }
    — { type: rsa, size: 2048 }

* в данном примере мы создадим 4 ключа разных типов: dsa, ecdsa, ed25519, rsa. Так как у каждого из них свои требования к размеру, перечень представлен в виде двумерного массива. Ключи будут созданы в каталоге /etc/ssh/.

О openssh_keypair: https://docs.ansible.com/ansible/latest/collections/community/crypto/openssh_keypair_module.html.

3. Работа с SSH authorized_key.

Данный файл содержит публичный ключ для подключения по SSH. Работа с ним через Ansible выполняется с помощью модуля authorized_key.

Пример добавления ключа:

— name: Set authorized key took from file
  authorized_key:
    user: root
    state: present
    key: ‘{{ item }}’
  with_file:
    — files/key.pub

* в данном примере мы берем содержимое файла files/key.pub и устанавливаем его для пользователя root.

Об authorized_key: https://docs.ansible.com/ansible/2.4/authorized_key_module.html.

4. Создание самоподписанного сертификата.

Чтобы получить, более или менее, корректный самоподписанный сертификат, нам нужно будет воспользоваться тремя модулями ansible:

  • openssl_privatekey — создаст приватный ключ.
  • openssl_csr — создаст запрос для выпуска открытого сертификат по заданным параметрам на основе закрытого ключа.
  • openssl_certificate — сгенерирует открытый сертификат на основе ключа запроса.

Рассмотрим использование данных модулей на конкретном примере:

— name: Создаем закрытый ключ (key)
  openssl_privatekey:
    path: /etc/ssl/mycert.key
    size: 2048

— name: Создаем файл запроса (csr)
  openssl_csr:
    path: /etc/ssl/mycert.csr
    privatekey_path: /etc/ssl/mycert.key
    country_name: RU
    locality_name: Russian Federation
    organization_name: Dmosk
    email_address: master@dmosk.ru
    common_name: ansible.dmosk.ru
    subject_alt_name:
      — «DNS:ansible.dmosk.ru»
      — «DNS:test.dmosk.ru»

— name: Создаем самоподписанный сертификат (crt)
  openssl_certificate:
    path: /etc/ssl/mycert.crt
    csr_path: /etc/ssl/mycert.csr
    privatekey_path: /etc/ssl/mycert.key
    provider: selfsigned

* в нашем запросе мы в итоге получим ключи mycert.key и mycert.crt в каталоге /etc/ssl. Результат будет похожим, если бы мы ввели команду openssl req -nodes -new -x509 -keyout /etc/ssl/mycert.key -out /etc/ssl/mycert.crt -subj ‘/C=RU/L=Russian Federation/O=Dmosk/CN=ansible.dmosk.ru’.

Виртуализация VMware

Работа с виртуальными машинами на платформе VMware выполняется с помощью большого количества различных модулей vmware. С полным их списком можно ознакомиться на странице https://docs.ansible.com/ansible/latest/collections/community/vmware/index.html.

Мы рассмотрим несколько примеров. В них мы будем использовать следующие переменные:

  1. vcenter_hostname — имя сервера vcenter или его IP-адрес.
  2. vcenter_username — логин для подключения к vcenter.
  3. vcenter_password — пароль для подключения. Лучше хранить с использованием vailt.
  4. vcenter_datacenter — имя датацентра, к которому нужно подключаться.

Данные переменные необходимо определить заранее. Это можно сделать при вызове плейбука, описать в самом сценарии или роли. Подробнее о работе с переменными смотрите раздел выше.

1. Модуль vmware_guest

С его помощью мы можем взаимодействовать с гостевой операционной системой виртуальной машины. Необходимо позаботиться, чтобы на последней были установлены VMware Tools.

Подробнее о vmware_guest: https://docs.ansible.com/ansible/latest/collections/community/vmware/vmware_guest_module.html.

а) Базовое подключение.

Для выполнения действий над виртуальными машинами мы должны подключиться к хосту VMware. Для этого используем данные строки:

— name: Connect to ESX Host
  vmware_guest:
    hostname: «{{ vcenter_hostname }}»
    username: «{{ vcenter_username }}»
    password: «{{ vcenter_password }}»
    validate_certs: no

* параметр validate_certs, выставленный в no, позволит избежать ошибки, если у нас на хосте используется самоподписанный сертификат (как правило, так и есть).

б) Переименовать виртуальную машину.

Для выполнения действия нам нужно знать идентификатор виртуальной машины:

— name: Rename a virtual machine
  vmware_guest:
    hostname: «{{ vcenter_hostname }}»
    username: «{{ vcenter_username }}»
    password: «{{ vcenter_password }}»
    validate_certs: no
    uuid: «{{ hw_product_uuid }}»
    name: «Template-{{ vm_name }}»
    state: present

* где uuid — идентификатор виртуальной машины; name — новое имя виртуальной машины.

в) Конвертировать виртуальную машину в шаблон.

Для этого нужно просто задать признак is_template:

— name: Convert virtual machine to Template
  vmware_guest:
    hostname: «{{ vcenter_hostname }}»
    username: «{{ vcenter_username }}»
    password: «{{ vcenter_password }}»
    validate_certs: no
    uuid: «{{ vm_info.instance.hw_product_uuid }}»
    is_template: true
    state: present

2. Модуль vmware_vm_info

Позволяет собирать информацию о виртуальных машинах на хосте VMware.

О модуле vmware_vm_info: https://docs.ansible.com/ansible/latest/collections/community/vmware/vmware_vm_info_module.html.

а) Получение информации по всем виртуальным машинам на хосте:

— name: Gather all registered virtual machines
  vmware_vm_info:
    hostname: ‘{{ vcenter_hostname }}’
    username: ‘{{ vcenter_username }}’
    password: ‘{{ vcenter_password }}’
    validate_certs: no
  register: vminfo

— debug:
    var: vminfo.virtual_machines

* всю информацию мы запишем в переменную vminfo и выведем ее на экран.

б) Чтобы получить информацию не о всех машинах, а о конкретной, то добавляем опцию vm_name:

— name: Gather all registered virtual machines
  vmware_vm_info:
    hostname: ‘{{ vcenter_hostname }}’
    username: ‘{{ vcenter_username }}’
    password: ‘{{ vcenter_password }}’
    validate_certs: no
    vm_name: mail-vm
  register: vminfo

3. Модуль vmware_guest_info

Позволяет получить подробную информацию о виртуальной машине.

О модуле vmware_guest_info: https://docs.ansible.com/ansible/latest/collections/community/vmware/vmware_guest_info_module.html.

а) Получение информации о конкретной виртуальной машине:

— name: Gather virtual machine
  vmware_guest_info:
    hostname: ‘{{ vcenter_hostname }}’
    username: ‘{{ vcenter_username }}’
    password: ‘{{ vcenter_password }}’
    datacenter: ‘{{ vcenter_datacenter }}’
    validate_certs: no
    name: ‘{{ vm_name }}’
  register: vminfo

— debug:
    var: vminfo.instance

* информация будет собрана для виртуальной машины с именем, которое определено в переменной vm_name.

4. Модуль vmware_guest_powerstate

Позволяет включать, выключать и перезагружать виртуальные машины.

О модуле vmware_guest_powerstate: https://docs.ansible.com/ansible/latest/collections/community/vmware/vmware_guest_powerstate_module.html.

а) Включение виртуальной машины:

— name: ‘Start {{ vm_name }}’
  vmware_guest_powerstate:
    hostname: «{{ vcenter_hostname }}»
    username: «{{ vcenter_username }}»
    password: «{{ vcenter_password }}»
    name: «{{ vm_name }}»
    state: powered-on
    validate_certs: no

б) Выключение виртуальной машины:

— name: ‘Shutdown {{ vm_name }}’
  vmware_guest_powerstate:
    hostname: «{{ vcenter_hostname }}»
    username: «{{ vcenter_username }}»
    password: «{{ vcenter_password }}»
    name: «{{ vm_name }}»
    state: shutdown-guest
    state_change_timeout: 200
    validate_certs: no

Или грубо:

— name: Set the state of a virtual machine to poweroff
  vmware_guest_powerstate:
    hostname: «{{ vcenter_hostname }}»
    username: «{{ vcenter_username }}»
    password: «{{ vcenter_password }}»
    name: «{{ vm_name }}»
    state: powered-off
    validate_certs: no

в) Перезагрузка:

— name: ‘Restart {{ vm_name }}’
  vmware_guest_powerstate:
    hostname: «{{ vcenter_hostname }}»
    username: «{{ vcenter_username }}»
    password: «{{ vcenter_password }}»
    name: «{{ vm_name }}»
    state: reboot-guest
    state_change_timeout: 200
    validate_certs: no

Или грубо:

— name: Set the state of a virtual machine to restarted
  vmware_guest_powerstate:
    hostname: «{{ vcenter_hostname }}»
    username: «{{ vcenter_username }}»
    password: «{{ vcenter_password }}»
    name: «{{ vm_name }}»
    state: restarted
    validate_certs: no

Виртуализация Proxmox

Рассмотрим немного примеров по работе с системой виртуализации proxmox.

Для работы с данной системой виртуализации есть в Ansible специализированный модуль proxmox_kvm. Его недостаток — относительно, небольшой перечень возможностей. Поэтому часть задачь будет решаться с помощью API.

Мы будем использовать следующие переменные:

  1. pve_host — имя сервера pve или его IP-адрес.
  2. pve_user — логин для подключения к vcenter.
  3. pve_password — пароль для подключения. Лучше хранить с использованием vailt.
  4. pve_node — имя ноды в датацентре.
  5. pve_port — номер порта, на котором слушает гипервизор (по умолчанию 8006).

Перейдем к примерам.

1. Сбор информации

В данном разделе мы рассмотрим примеры для получения информации с Proxmox.

а) Список виртуальных машин на хосте. Для получения информации о виртуальных машинах, которые находятся на хосте виртуализации нужно использовать API Proxmox. Для этого в ansible мы будем применять модуль URI:

— name: Get vms from pve
  uri:
    url: «https://pve.dmosk.local:8006/api2/json/cluster/resources?type=vm»
    headers:
      Authorization: PVEAPIToken=ansible@pve!Ansible=e94d5627-1f8d-36a7-37e2-7ad6fad65ab7
    follow_redirects: all
  register: vm_list

* где:

  • pve.dmosk.local — адрес веб-интерфейса сервера виртуализации.
  • 8006 — порт для подключения в веб-интерфейсу.
  • ansible@pve — имя учетной записи, для которой создан токен доступа. Он создается в консоле управления Proxmox (ДатацентрРазрешенияAPI Tokens).
  • Ansible=e94d5627-1f8d-36a7-37e2-7ad6fad65ab7 — имя токена и сам токен.

В результате мы сохраним список виртуальных машин в переменной vm_list.

б) Подробная информация о виртуальной машине или хосте виртуализации. Если мы хотим собрать побольше информации, нам также понадобится API:

— name: Get vm info
  uri:
    url: «https://pve.dmosk.local:8006/api2/json/<запрос>»
    headers:
      Authorization: PVEAPIToken=ansible@pve!Ansible=e94d5627-1f8d-36a7-37e2-7ad6fad65ab7
    follow_redirects: all
  register: vm_info

Для получения различной информации о виртуальной машине или сервере мы используем различные запросы (отмечено как <запрос>). С полным списком того, что мы можем получить предлагаю ознакомиться на странице pve.proxmox.com/pve-docs/api-viewer.

Например, для получения конфигурации виртуальной машины, используем запрос:

nodes/<имя ноды>/qemu/<VMID>/config

Чтобы узнать hostname:

nodes/<имя ноды>/qemu/<VMID>/agent/get-host-name

И так далее.

в) Информация о машине с помощью модуля proxmox_kvm:

— name: «Get VMID»
  proxmox_kvm:
    node: «{{ pve_node }}»
    api_user: «{{ pve_user }}»
    api_password: «{{ pve_password }}»
    api_host: «{{ pve_host }}»
    name: «myvm»
    state: current
  register: vm_info

2. Операции с ВМ

Рассмотрим наиболее популярные действия над виртуальными машинами.

*) Остановить:

— name: «Останавливаем виртуальную машину Proxmox»
  proxmox_kvm:
    api_host    : «{{ pve_host }}»
    api_user    : «{{ pve_user }}»
    api_password: «{{ pve_password }}»
    node        : «{{ pve_node }}»
    name        : «myvm»
    state       : stopped
    force       : yes
    timeout     : 300

*) Удалить:

— name: «Удаляем виртуальную машину Proxmox»
  proxmox_kvm:
    api_host     : «{{ pve_host }}:{{ pve_port }}»
    api_user     : «{{ pve_user }}»
    api_password : «{{ pve_password }}»
    node         : «{{ pve_node }}»
    name         : «myvm»
    state        : absent

Работа с Docker

В данном разделе мы рассмотрим работу с контейнерами Docker.

Для корректной работы модуля, на целевом хосте должен быть установлен компонент python docker:

pip3 install docker

Обратите внимание, что должен использоваться python версии 3. Для версии 2 уже возникают ошибки при установке и работе модуля.

На стороне, где запускается ansible необходимо установить коллекцию docker:

ansible-galaxy collection install community.docker

1. Работа с контейнером.

С помощью модуля docker_container мы можем работать с контейнерами. Создавать их, удалять, перезапускать и так далее. Рассмотрим несколько примеров.

а) Создать новый контейнер можно с помощью сценария:

— name: Create a new container
  docker_container:
    name: new_container
    image: nginx

* в данном примере будет создан контейнер с именем new_container из образа nginx.

Создание контейнера с большим количиством опций:

— name: Create a new container
  docker_container:
    name: new_container
    image: nginx
    auto_remove: yes
    volumes:
      — «/tmp:/tmp»
    ports:
      — «80:80»

* где:

  • auto_remove — удалить контейнер после завершения его работы.
  • volumes — создаем volume. В данном случае, проброс каталога /tmp внутрь контейнера тоже в /tmp.
  • ports — на каком порту должна слушать хостовая система и на какой порт пробрасывать запросы внутрь контейнера.

б) Для удаления используем:

— name: Delete container
  docker_container:
    name: new_container
    state: absent

* важно указать имя контейнера и state со значением absent.

О docker_container: https://docs.ansible.com/ansible/2.9/modules/docker_container_module.html.

2. Запуск команды внутри контейнера.

Данное действие эквивалентно команде docker exec. В ansible можно выполнить с помощью модуля docker_container_exec или ранее рассмотренного docker_container. Рассмотрим оба варианта.

а) docker_container_exec.

Пример использования:

— name: Download wp-cli utility
  community.docker.docker_container_exec:
    container: «wordpress»
    command: curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
    chdir: /root

* в нашем примере мы скачаем утилиту wp-cli в контейнере wordpress. Команда будет запущена в директории chdir.

О docker_container_exec: https://docs.ansible.com/ansible/latest/collections/community/docker/docker_container_exec_module.html.

б) docker_container.

Данный способ подойдет в случае, когда нам нужно создать контейнер, запустить его и выполнить команду. Например:

— name: Create a new container
  docker_container:
    name: new_container
    image: nginx
    command: whoami

Облачная платформа OpenStack

В данном разделе рассмотрим модуль работы с платформой виртуализации OpenStack. Данный модуль не входит в стандартный комплект и необходимо выполнить предварительные настройки для системы.

Устанавливаем коллекцию openstack.cloud на компьютер, где запускается ansible:

ansible-galaxy collection install openstack.cloud

Ознакомиться со всеми модулями данной коллекции можно на странице с документацией Ansible.

Также необходим python версии 3 с модулем openstacksdk. Установим данный модуль на компьютере, где будет запускаться сценарий:

python3 -m pip install —upgrade pip

python3 -m pip install openstacksdk

Для подключения и работы с OpenStack необходимы данные, характерные для хостинг-провайдера облачной услуги. Для этого изучите документацию по работе с OpenStack CLI на официальном сайте последнего. Также немного о работе с OpenStack CLI на моем сайте.

Для работы с модулями группы openstack нам нужно будет проходить авторизацию. Для этого мы будем использовать переменные, например:

    cloud:
      auth_url: https://infra.mail.ru:35357/v3/
      username: cloud-user-name
      password: cloud-user-password
      project_id: cloud-project-id
      user_domain_name: cloud-user-domain

* где:

  • auth_url — ссылка для подключения к облаку по openstack api.
  • username — учетная запись для прохождения аутентификации.
  • password — пароль от учетной записи.
  • project_id — идентификатор проекта.
  • user_domain_name — домен пользователей.

И так, рассмотрим простые примеры. Для удобства, попробуем разбить информацию по разделам.

Работа с виртуальными машинами

Используем модуль openstack.cloud.server.

Об openstack server: https://docs.ansible.com/ansible/latest/collections/openstack/cloud/server_module.html.

1. Создание инстанса:

— name: Создать виртуальную машину в облаке VK Cloud
  openstack.cloud.server:
    state: present
    auth:
      auth_url: «{{ cloud.auth_url }}»
      username: «{{ cloud.username }}»
      password: «{{ cloud.password }}»
      project_id: «{{ cloud.project_id }}»
      user_domain_name: «{{ cloud.user_domain_name }}»
    name: vm-name
    region_name: cloud-region-name
    image: cloud-image-id
    key_name: key-ssh-name
    timeout: 200
    flavor: cloud-vm-template
    availability_zone: MS1
    nics:
      — net-id: cloud-network-id
    security_groups:
      — default
      — ssh
    meta:
      hostname: my-cloud-vm-01

* обратите внимание, что некоторые опции не будут работать для вашего поставщика облачной услуги. Их нужно заменить на другие, а какие именно, придется изучить соответсвующую документацию, представленную на сайте последнего.

2. Удаление виртуальной машины:

— name: Удалить виртуальную машину в облаке openstack
  openstack.cloud.server:
    state: absent
    auth:
      auth_url: «{{ cloud.auth_url }}»
      username: «{{ cloud.username }}»
      password: «{{ cloud.password }}»
      project_id: «{{ cloud.project_id }}»
      user_domain_name: «{{ cloud.user_domain_name }}»
    name: vm-name

3. Используем cloud-config. Большинство облачных провайдеров используют образы операционных систем с установленным cloud-init. Данный компонент позволяет автоматизировать некоторые операции по администрированию системы. Например, мы можем при создании виртуальной машины выполнить дополнительные настройки:

— name: Создать виртуальную машину в облаке VK Cloud + выполнить предварительные настройки
  openstack.cloud.server:
    state: present
    …
    config_drive: true
    userdata: |
      #cloud-config
      users:
        — name: dmosk
          groups: [ sudo ]
          shell: /bin/bash
          lock_passwd: false
          passwd: «$6$ssBWwe7wxa.lPToS$P5gm0y13m3X8VsK.czScN0ilTxYrB86KNhv7YvD4.4f0CdTDvfAV3W95pYQCPUdF2iRtIXBSVIw/ZYnxtBvOA.»
      ssh_pwauth: true
      hostname: «my-cloud-vm-01»

* в данном примере нужно обратить внимание на следующие моменты:

  • config_drive — по умолчанию, cloud-init берет информацию для настройки с сервера. Данный параметр указывает, что должен использоваться пользовательский конфиг.
  • users — создаем пользователей в системе.
    • Некоторые провайдеры не позволяют создавать учетные записи с логином admin.
    • Хэш для passwd можно получить командой mkpasswd -m sha-512.
  • ssh_pwauth — по умолчанию на сервер нельзя зайти по ssh с использованием парольной аутентификации. Данная опция контролирует это поведение.

Получение информации

В данном разделе мы будем использовать следующие модули:

  • server_info — позволит получить информацию о виртуальной машине.
  • resources — сведения о различных ресурсах в облаке.
  • image_info — информация об образах для виртуальных дисков. 

Рассмотрим следующие примеры.

1. Получение информации о виртуальной машине. Выполняем с помощью модуля server_info:

— name: Получаем информацию о виртуальной машине vm1
  openstack.cloud.server_info:
    auth:
      auth_url: «{{ cloud.auth_url }}»
      username: «{{ cloud.username }}»
      password: «{{ cloud.password }}»
      project_id: «{{ cloud.project_id }}»
      user_domain_name: «{{ cloud.user_domain_name }}»
    name: vm1
  register: openstack

— debug:
    var: openstack

2. Список виртуальных машин. Используем модуль resources:

— name: Получаем список виртуальных машин
  openstack.cloud.resources:
    auth:
      auth_url: «{{ cloud.auth_url }}»
      username: «{{ cloud.username }}»
      password: «{{ cloud.password }}»
      project_id: «{{ cloud.project_id }}»
      user_domain_name: «{{ cloud.user_domain_name }}»
    service: compute
    type: server
  register: vm_list

3. Список образов для виртуальных дисков. Используем image_info.

а) все образы:

— name: Получаем список образов
  openstack.cloud.image_info:
    auth:
      auth_url: «{{ cloud.auth_url }}»
      username: «{{ cloud.username }}»
      password: «{{ cloud.password }}»
      project_id: «{{ cloud.project_id }}»
      user_domain_name: «{{ cloud.user_domain_name }}»
  register: images_list

б) конкретный образ:

  — name: Получаем информацию об образе «Ubuntu-22.04-202208»
    openstack.cloud.image_info:
      auth:
        auth_url: «{{ cloud.auth_url }}»
        username: «{{ cloud.username }}»
        password: «{{ cloud.password }}»
        project_id: «{{ cloud.project_id }}»
        user_domain_name: «{{ cloud.user_domain_name }}»
      image: Ubuntu-22.04-202208
    register: image_info

Работа с базами данных

В данном блоке рассмотрим примеры работы с различными базами данных.

 MySQL /MariaDB

Работа с базой данных возможна с помощью различных коллекций:

  • mysql_db — позволяет редактировать базу данных или создавать дамп.
  • mysql_query — создание запросов.
  • mysql_user — работа с ролями.

Полный перечень модулей для работы с MySQL /MariaDB можно посмотреть на странице docs.ansible.com/ansible/latest/collections/community/mysql.

Они не идут в комплекте к ansible и нам необходимо установить их командой:

ansible-galaxy collection install community.mysql

Теперь рассмотрим несколько примеров для работы с MySQL / MariaDB через ansible.

1. Резервное копирование.

Для создания дампа используем сценарий:

— name: Dump mysql databases
  community.mysql.mysql_db:
    state: dump
    name:
      — db1
      — db2
    target: /tmp/dump.sql

* в данном примере мы создадим 2 дампа из баз db1 и db2 и сохраним результат в файл /tmp/dump.sql.

Для восстановления из дампа:

— name: Restore mysql databases
  community.mysql.mysql_db:
    name: db1
    state: import
    target: /tmp/dump.sql

2. Создать базу:

— name: Создаем базу данных для хранения данных почтового сервера
  community.mysql.mysql_db:
    name: postfix
    state: present
    encoding: utf8
    collation: utf8_general_ci

* создадим базу данныз с названием postfix и кодировкой UTF-8.

3. Создаем пользователя:

— name: Создаем пользователя и даем ему права на базу postfix
  community.mysql.mysql_user:
    state: present
    name: postfix
    password: postfix123
    priv: ‘postfix.*:ALL’

PostgreSQL

Рассмотрим разные примеры работы с СУБД PostgreSQL. Мы будем использовать следующие модули:

  • postgresql_db. Работа с базами данных.
  • postgresql_query. Создание запросов.
  • postgresql_set. Внесение изменений в настройки СУБД.

Обратите внимание, что нам понадобится установка модуля командой:

ansible-galaxy collection install community.postgresql

Подробнее можно найти инструкцию по ссылке выше.

1. Резервное копирование:

— name: Create dump
  postgresql_db:
    name: DATABASE
    login_host: SERVER
    port: PORT
    login_user: LOGIN
    login_password: PASSWORD
    state: dump
    target: ‘/tmp/dump.sql’

2. Восстановление из дампа:

Сценарий напоминает резервное копирование, за исключением опции state:

— name: Create dump
  postgresql_db:
    name: DATABASE
    login_host: SERVER
    port: PORT
    login_user: LOGIN
    login_password: PASSWORD
    state: restore
    target: ‘/tmp/dump.sql’

3. Выполнение запроса:

Запрос в базу можно сделать с помощью модуля postgresql_query:

— name: Select query
  postgresql_query:
    query: «SELECT * FROM users»
    db: DATABASE
    login_host: SERVER
    port: PORT
    login_user: LOGIN
    login_password: PASSWORD

Рассмотрим конкретный пример с сохранением результата в переменную:

— name: Select query
  postgresql_query:
    query: «SELECT * FROM users WHERE name = ‘%s’ or family = ‘%s'»
    db: clients
    login_host: localhost
    port: 5432
    login_user: dbuser
    login_password: dbpassword
    positional_args:
      — Andrey
      — Ivanov
  register: myclients

— name: Set variable
  set_fact:
    my_clients: myclients.query_result

* что мы сделали:

  • подключились к серверу баз данных на локальном сервере под пользователем dbuser с паролем dbpassword.
  • сделали запрос к базе clients. Запрос должен получить список всех записей из таблицы users, где в поле name есть значение Andrey или в поле family — Ivanov. Обратите внимание, что мы использовали паттерны с применением positional_args.
  • результат работы задачи мы зафиксировали в переменной myclients.
  • создали переменную my_clients, куда занесли результыты выборки.

4. Конфигурирование PostgreSQL. С помощью модуля postgresql_set можно вносить изменения в конфигурацию СУБД на подобие команды ALTER SYSTEM.

Например:

— name: Включаем SSL в PostgreSQL
  postgresql_set:
    name: ssl
    value: ‘on’
    login_unix_socket: »
  become: yes
  become_user: postgres

* в данном примере мы разрешим SSL на PostgeSQL. Обратите внимание на директиву login_unix_socket — она указывает на папку, где находится сокет подключения к СУБД. Его использует встроенная учетная запись postgres, чтобы подключаться к базе. По умолчанию, ansible ищет сокетный файл .s.PGSQL.5432 в каталоге /var/run/postgresql, но в некоторых случаях, например, при работе с Postgres Pro, данный путь может отличаться. У меня он был /tmp — в таком случае путь нужно указать явно, в противном случае мы получим ошибку unable to connect to database: connection to server on socket \»/var/run/postgresql/.s.PGSQL.5432\» failed: No such file or directory\n\tIs the server running locally and accepting connections on that socket?\n.

Запуск плейбука

Чтобы запустить плейбук, используем команду ansible-playbook.

Рассмотрим отдельно некоторые возможности при ее запуске.

1. Запуск плейбука с указанием файла инвентаризации:

ansible-playbook -i ./inventory.yml ./playbook.yml

* это простая команда запустит плейбук playbook.yml, с указанием необходимости использовать файл инвентаризации inventory.yml.

2. Начинать выполнение с определенной задачи.

При выполнении отладки, полезно запустить плейбук, но начать выполнение с определенной задачи. Остальные пропустить. 

Это можно сделать с помощью опции —start-at-task:

ansible-playbook … —start-at-task=»Start Job»

* в данном примере плейбук начнет выполнять задания с задачи Start Job.

3. Передача переменных.

Мы можем определить переменные при запуске плейбука с помощью опции —extra-vars. Возможна передача одной или нескольких переменных:

ansible-playbook … —extra-vars «{ ‘domain_name’:’dmosk.local’, ‘ver’:’5′, ‘vm_name’:’test’ }»

4. Использование тегов.

Теги позволяют пропускать или выполнять определенные задания. Предположим, у нас есть задание:

— name: Install postfix
  package:
    name: postfix
    state: present
  tags:
    — init postfix

* в данном примере мы навесили тег init postfix на задание Install postfix.

Теперь мы можем запустить плейбук:

ansible-playbook … —tags «init postfix»

И задание будет выполнено. 

Или так:

ansible-playbook … —skip-tags «init postfix»

И задание будет проигнорировано.

О тегах: https://docs.ansible.com/ansible/latest/user_guide/playbooks_tags.html.

Обработка ошибок

В процессе работы мы сталкиваемся с проблемами в отработки сценариев. Можно настроить поведение при выполнении заданий, если что-то пошло не так.

1. Игнорировать ошибки.

Если ansible столкнется с ошибкой при выполнении задачи, работа плейбука будет завершена. Иногда, нужно пропустить ошибку при выполнении определенной задачи, чтобы выполнение было продолжено. Для этого существует опция ignore.

а) чтобы пропустить ошибки выполнения, в настройка задачи используем:

— name: Bad Task
  …
  ignore_errors: yes 

б) чтобы игнорировать ошибки при подключении к хосту:

— name: Bad Task
  …
  ignore_unreachable: yes 

в) также мы можем проигнорировать ошибку по условию, например:

— name: Выполняем shell
  shell: …
  register: result
  failed_when: >
    result.rc != 0 and
    ‘already exists’ not in result.stderr

* в данном примере мы выполняем какую-то каманду, и если она закончится с ошибкой, в сообщении которой есть фраза «already exists», ansible не воспримет это за ошибку и продолжит выполнение сценария.

2. Выбрасывать ошибку по своему условию.

Мы можем задать свое условие, когда Ansible должен считать, что задача отработана неправильно и нужно остановить работу. Это делается с помощью модуля fail.

а) выкинуть ошибку, если переменная не равна определенному значению:

— name: Fail if my data not loaded
  fail:
    msg: My data has not been loaded completely.
  when: my_dataload_status != «complete»

* в данном примере если переменная my_dataload_status не равна complete, системв вернет ошибку.

б) ошибка, если переменная содержит запрещенный символ:

— name: Fail if * in var
  fail:
    msg: «Ошибка — в переменной login есть символ *»
  when: ‘»*» in login’

О fail: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/fail_module.html.

3. Действия в случае ошибки.

Рассмотрим 2 варианта:

  1. Мы, в любом случае, хотим выполнить чать задачь, даже если сценарий прекратит работу из-за ошибки.
  2. Мы хотим выполнить определенное действие, если сценарий прекратит работу из-за ошибки.

Это возможно реализовать с помощью block + rescue/always.

О blocks: https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_blocks.html.

Рассмотрим пример.

а) Выполнять задачу после rescue, если основной сценарий выполнился с ошибкой:

— name: Основной сценарий
  block:
    …
  rescue:
    — name: Выполнить, если произошла ошибка
      debug:
        msg: ‘Show error’

б) Выполнять задачу после always независимо от результата выполнения основного сценария:

— name: Основной сценарий
  block:
    …
  always:
    — name: Выполнить в любом случае
      command: rm -f /tmp/test

в) И то и другое:

— name: Основной сценарий
  block:
    …
  rescue:
    — name: Выполнить, если произошла ошибка
      debug:
        msg: ‘Show error’
  always:
    — name: Выполнить в любом случае
      command: rm -f /tmp/test

* в данном примере мы отобразим текст Show error только при наличии ошибки, а также выполним команду rm -f /tmp/test независимо от исхода работы сценария.

Работа с выводом и строками

В данном разделе будут рассмотрены варианты манипуляции данными — парсинг, фильтры, обработки и подобные действия. Так как ansible является декларативным языком, описанные операции лучше свести к минимуму — автору показалась такая работа не совсем удобной.

Конвертация типов данных

От типа данных зависят операции и способы работы с ними. Приведу пример типов данных, с которыми мы можем столкнуться:

  • Строка — набор символов. Заключаются в кавычки.
  • Число — набор цифр. Записываются без кавычек.
  • Список (list) — массив данных. В качестве ключа используется пронумерованный индекс. Записываются в квадратных скобках.
  • Словарь (dict) — массив данных. В качестве ключа используется символный индекс. Записываются в фигурных скобках.
  • JSON — строка, имеющая формат записи в виде словаря. Считается удобным для передачи данных между системами.
  • YAML — строка, в которой данные разделяются переносами, а вложенность определяется отступами. Формат строго зависит от последних — лишний пробел нарушает его обработку. Удобно использовать для наглядного вывода информации.

Рассмотрим примеры преобразований данных из одного типа в другой.

1. Строку в число. Преобразуем с помощью int:

«100» | int

2. Список в json. Можно выполнить с помощью to_json:

list_var | to_json

или в json, который будет удобно читать:

list_var | to_nice_json

Преобразование строк

Полученные строки могут требовать дальнейшего разбора, например, парсинга. Рассмотрим несколько примеров.

1. Получение массива с помощью split.

Метод split позволяет разбить строку на части по определенному символу. Полученные части станут значениями массива:

— name: Set variable
  set_fact:
    newvar: «{{ results.split(‘:’) }}»

* в данном примере мы разобьем строку из переменной results на части по двоеточию и сохраним все это в переменную newvar.

2. Замены.

Мы рассмотрим несколько способов.

а) C помощью replace.

Позволяет в нашем выводе заменить одни символы другими:

— name: Set variable
  set_fact:
    newvar: «{{ results | replace(‘ ‘, ») }}»

* тут в переменную newvar будет записана строка results, из которой мы уберем все пробелы.

б) с помощью regex_replace.

Если нам нужно использовать регулярные выражения, то используем regex_replace. Например:

— name: Set variable
  set_fact:
    newvar: «{{ results | regex_replace(‘^test’, ») }}»

… позволит искать слово test в начале строки.

3. Использование фильтров.

С помощью regex_search мы можем оставить определенную часть строки:

— set_fact:
    my_var: «{{ ‘my-chars-12’ | regex_search(‘\\d+’) }}»

В данном примере мы создадим переменную my_var, значением которой будет чисо 12 (из строки my-chars-12 мы получим число).

О regex_search: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/regex_search_filter.html.

Работа с массивами

В данном подразделе будем работать со списками и словарями.

1. Объединение.

Предположим, что у нас есть два списка из словарей:

vars:
  list1:
    — name: name1
      value1: value11
    — name: name2
      value1: value12
  list2:
    — name: name1
      value2: value21
    — name: name3
      value2: value22

Мы можем их объединить по одному из полей, например, name:

— debug:
        msg: «{{ list1 | community.general.lists_mergeby(list2, ‘name’) }}»

* для lists_mergeby обязательно полное написание — community.general.lists_mergeby.

В результате, мы получим такой массив:

{
  name: name1
  value1: value11
  value2: value21
},
{
  name: name2
  value1: value12
},
{
  name: name3
  value2: value22
}

* как видим, два массива слились в один. Для ключа name1 также добавлено поле value2.

О lists_mergeby: https://docs.ansible.com/ansible/devel/collections/community/general/lists_mergeby_filter.html.

2. Преобразование элемента списка в словарь.

Предположим, у нас есть:

vars:
  list:
    — key: key1
      value: value1
    — key: key2
      value: value2
    — key: key3
      value: value3

Мы можем его преобразовать в словарь по имеющимся ключам (key).

а) Если имя ключа для ключа key, а имя ключа для значения value:

— debug:
    msg: «{{ list | items2dict }}»

Мы должны увидеть что-то на подобие:

{
  «key1»: «value1»,
  «key2»: «value2»,
  «key3»: «value3»
}

б) Если имя ключа для ключа НЕ key, а имя ключа для значения НЕ value:

— debug:
    msg: «{{ list | items2dict(key_name=’name’, value_name=’model’) }}»

* в данном примере предполагается, что у нас есть массив с ключами name и model. Мы хотим, чтобы в качестве ключа для создаваемого словаря использовался первый, а для значения второй.

О items2dict: https://docs.ansible.com/ansible/devel/collections/ansible/builtin/items2dict_filter.html.

Формат JSON

Посмотрим, что мы можем сделать с json.

1. Фильтр данных. С помощью json_query мы можем выбирать только те данные, которые соответствуют критерию поиска.

а) один критерий:

— name: Display json filtered data
  debug:
    var: item
  loop: «{{ my_json | from_json | json_query(json_query_string) }}»
  vars:
    json_query_string: >-
      [?state==’running’]

* в данном примере мы сделаем выборку всех данных, где значение поля state равно running.

б) два критерия:

— name: Display json filtered data
  debug:
    var: item
  loop: «{{ my_json | from_json | json_query(json_query_string) }}»
  vars:
    json_query_string: >-
      [?state==’running’ && code==`200`]

* в данном примере мы добавили критерий code==`200`. Обратите внимание, что в нем мы используем другой тип кавычек. Это сделано не просто так — для строк используются кавычки » или ‘, для цифр — `.

в) фильтр с выбором конкретных полей:

— name: Display json filtered data
  debug:
    var: item
  loop: «{{ my_json | from_json | json_query(json_query_string) }}»
  vars:
    json_query_string: >-
      [?state==’running’ && code==`200`].{key1: key1, key2: key2, key3: key3}

* в этои примере мы взяли ранее использовавшейся фильр и перечислили конкретные поля, которые мы хотим показать при выводе информации.

Import и include 

Данные метода позволяют подключить роль, задачу или плейбук. При этом, у методов разное поведение:

  • include_*: обрабатываются по ходу выполнения сценария.
  • import_*: предварительно обрабатываются во время анализа сценария.

В зависимости от ситуации, удобнее применять тот или иной метод. Например, если у нас часть код написан для разных версий Ansible, то import закончится с ошибкой, так как интерпретатор будет смотреть все файлы. Мы можем обойти данную проблему, используя include — тогда чать код будет интерпретироваться во время выполнения и можно будет указать направление чтения сценария с помощью условий.

Об import и include: https://docs.ansible.com/ansible/2.9/user_guide/playbooks_reuse_includes.html.

Импорт ролей 

— name: «Include Other Role»
  include_role:
    name: other_role

А это пример, как подключить роль и сделать так, чтобы все ее задачи выполнились на определенном хосте:

— name: «Include Other Role»
  include_role:
    name: other_role
    apply:
      delegate_to: «{{ deploy_vm.instance.ipv4 }}»

Еще один способ включить роль, но выполнить задания из другого файла:

— name: «Include Other Role From new_roles File»
  include_role:
    name: other_role
    tasks_from: new_roles

* в данном примере будут использовать файл не main.yml, а new_roles.yml.

Импорт задач

В рамках одной и той же роли можно использовать include_tasks:

— name: Загружаем задачи из файла, который находится в той же роле
  include_tasks: tasks2.yml

А тут мы подключим задачи в зависимости от версии ansible:

— name: Загружаем задачи для более новыйх версий ansible
  include_tasks: tasks_new.yml
  when: ansible_version >= 2.9

— name: Загружаем задачи ansible версии ниже 2.9
  include_tasks: tasks_old.yml
  when: ansible_version < 2.9

С помощью аргументов args + apply + delegate_to мы можем загрузить задачи из другого файла и выполнить их на другом хосте:

— name: Загружаем задачи из файла и выполняем их на хосте 1.2.3.4
  include_tasks: tasks_om_remote_host.yml
  args:
    apply:
      delegate_to: «1.2.3.4»

Шаблоны Jinja2

Помимо сценария с заданиями, при автоматизации сильно помогает шаблонизатор Jinja2. С его помощью можно на лету формировать файл с любым содержимым, которое должно быть сформировано динамически. Также шаблоны jinja можно применять в самих заданиях. Рассмотрим это на нескольких примерах.

1. Перебор массива.

Предположим, нам нужно перебрать все элементы массива в шаблоне. Это можно сделать конструкцией:

{% for host in my_hosts %}
server «{{ host }}»
{% endfor %}

* в данном примере мы сделаем перебор по переменной my_hosts. Для каждого элемента массива будет создана строка со значением server <значение переменной>.

2. Задаем значение переменной с IF.

Допустим, наша переменная должна иметь значение, в зависимости от значения другой переменной. Для этого можно применить один из следующих синтаксисов:

vars:
    var1: «test{% if hostname %}-value{% endif %}»
    var2: «{% if hostname %}value1{% else %}value2{% endif %}»
    var3: «{% if hostname == ‘myhost’ %}value1{% else %}value2{% endif %}»
    var4: «{{‘value1’ if (hostname) else ‘value2’}}»

* мы рассмотрели следующие варианты:

  • var1 — если есть переменная hostname, то добавляем к test еще и -value. В итоге, получится test-value.
  • var2 — если есть переменная hostname, значение будет value1, в противном случае — value2.
  • var3 — если переменная hostname равна myhost, задаем для var3 значение value1, в противном случае — value2.
  • var4 — другой формат записи. Если есть переменная hostname, значение будет value1, в противном случае — value2.

Данный модуль позволяет выполнять различные действия с ansible.

О meta: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/meta_module.html.

1. Завершить выполнение плейбука после определенной задачи.

С помощью данной конструкции:

— name: Stop Play
  meta: end_play

Мы можем полностью остановить выполнение задач для хоста:

— name: Stop Play for Host
  meta: end_host

2. Запустить handlers.

Задания, описанные в notify запускаются после того, как отработают все роли. Но, бывает, что нужно некоторые задачи, скажем, перезапуск службы, выполнить раньше. Для этого можно использовать flush_handlers.

— meta: flush_handlers

Разное

В данном разделе будет рассказано о дополнительных опциях, которые позволяют менять поведение выполнения задач, добавляет функциональности или все то, для чего не найдена отдельная подходящая категория.

1. Шифрование строки.

С помощью ansible-vault мы можем шифровать файлы и папки. Это позволит нам хранить секреты не в открытом виде. Данные расшифровываются в момент выполнения задач.

Данной командой мы получаем шифрованную строку:

ansible-vault encrypt_string

Система запросит ввести дважды пароль и предложит ввести строку, которую нужно зашифровать. После мы должны нажать 2 раза Ctrl + D — мы получим строку, которая начинается с !Vault и различные символы. 

Для того, чтобы в момент выполнения задачи ansible расшифровал данные, при запуске плейбука мы должны указать ключ —ask-vault-pass:

ansible-playbook … —ask-vault-pass

Система потребует ввести пароль в консоль.

Также мы можем использовать файл с паролем. Тогда плейбук мы должны запустить с опцией vault-password-file:

ansible-playbook … —vault-password-file <путь до файла с паролем>

или использовать системную переменную ANSIBLE_VAULT_PASSWORD_FILE:

export ANSIBLE_VAULT_PASSWORD_FILE=/root/.ansible-vault-password

Для дешифровки строки можно создать файл, в который добавить строку типа:

$ANSIBLE_VAULT;1.1;AES256

Допустим, создан файл с названием decrypt.txt. Тогда для дешифровки вводим команду:

cat decrypt.txt | ansible-vault decrypt

Система запросит пароль, который мы указывали, когда шифровали данную строку.

Об ansible-vault: https://docs.ansible.com/ansible/latest/user_guide/vault.html.

2. Зависимые роли.

С помощью файла meta/main.yml в роли мы можем определить пред-роль, от которой зависит выполнение текущей роли. Для этого настраивается опция dependencies:

dependencies:
  — role: pred

3. Повторы и циклы при выполнении задач.

Мы можем управлять цикличностью выполнения задач с помощью различных модулей ansible. Рассмотрим их на примерах.

а) Повторный запуск задачи.

Выполняется с помощью retries (количиство повторов) и delay (задержка в секундах). Например, можно еще раз запустить задачу при возникновении ошибки:

— name: Run anything command
  command: /foo/bar/cmd
  register: result
  retries: 3
  delay: 60
  until: result is not failed

* в данном примере мы будем выполнять команду /foo/bar/cmd пока ее выполнение не закончится без ошибок. Количество повторов будет равен 3 с интервалом в 60 секунд.

Небольшой пример на странице https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html#retrying-a-task-until-a-condition-is-met.

б) Использование циклов.

Выше в примерах мы часто упоминали loop для цикличного запуска задач в разными вариантами выполнения, например:

— name: Create Directories with loop
  file:
    path: «{{ item }}»
    state: directory
  loop:
    — ‘/var/log/prometheus’
    — ‘/var/log/grafana’

* в данном примере задача будет выполнена два раза — для создания каталогов /var/log/prometheus и /var/log/grafana.

в) Циклы из списков:

— name: Задаем настройки для nginx
  replace:
    path: /etc/nginx/nginx.conf
    regexp: «^{{ item.key }}=(.*)»
    replace: ‘{{ item.key }}={{ item.value }}’
  with_dict: «{{ nginxcfg }}»

* подразумевается, что у нас есть переменная nginxcfg в виде списка (массива). В данном примере ansible пробежит по каждой записи данного списка и сделает соответствующую замену в конфигурационном файле nginx.conf.

г) Повтор задачи несколько раз.

Мы можем задать переменную, с помощью которой будем контролировать число повторов при выполнении задачи. Это может быть удобным, если нам нужно создать, например, несколько виртуальных машин.

vars:
  var_count: 5


loop: «{{ range(1, (1 | int+var_count)) | list }}»
loop_control:
  loop_var: var_count_item

* в данном примере мы создаем цикл от 1 до 5 (значение переменной var_count). Чтобы ключ для цикла item не конфликтовал с ключами других циклов, которые могут использоваться внутри задачи, меняем имя item (по умолчанию) на var_count_item с помощью директивы loop_var.

О loop: https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html.

4. Объединение задач в блоки.

Это позволит установить общие свойства и условие для нескольких задач. Такая форма записи уменьшит количиство строк и упростит восприятие.

Синтаксис записи:

— name: Block Name
  block:
     — name: Task 1
       …

     — name: Task 2
       …

     — name: Task 3
       …
  when: ansible_facts[‘distribution’] == ‘CentOS’
  become: true
  become_user: root
  ignore_errors: yes

* в данном примере будет выполнены 3 задачи, если выполнится одно условие, которое описывается не для задач, а для блока.

О block: https://docs.ansible.com/ansible/latest/user_guide/playbooks_blocks.html.

5. Обращения к DNS.

При помощи модуля dig мы можем отправлять запросы на серверы имен, получая сведения о доменных записях. Сценарии использования могут быть различные.

а) простой запрос на получение IP-адреса для А-записи:

— name: NSLOOKUP for domain dmosk.ru
  debug:
    msg: «{{ lookup(‘dig’, ‘www.dmosk.ru.’)}}»

* в данном примере мы получим адрес для узла www.dmosk.ru.

б) запрос на получение IP-адреса для А-записи через определенный DNS-сервер:

— name: NSLOOKUP for domain dmosk.ru over 8.8.8.8
  debug:
    msg: «{{ lookup(‘dig’, ‘www.dmosk.ru.’, ‘@8.8.8.8’)}}»

О dig: https://docs.ansible.com/ansible/latest/collections/community/general/dig_lookup.html.

6. Отправка POST запроса.

Ранее мы рассматривали модуль uri для скачивания файла. Однако, данный модуль может быть более полезным. Например, мы можем отправлять запросы к API:

— name: Send Post Request
  uri:
    url: https://api.dmosk.ru/api/v2/send_request
    method: POST
    body_format: form-urlencoded
    return_content: false
    body:
      id: 1
      name: Test
      action: Read
    headers:
      Authorization: Bearer 577f573d09a1949436e3a07f7e9de6c5

* в данном примере будет отправлен POST-запрос на адрес https://api.dmosk.ru/api/v2/send_request. В POST-данных мы отправим idname и action. Также дополнительно мы отправили заголовок Authorization.

Об uri: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/uri_module.html.

7. Дата и время.

Мы можем использовать встроенную переменную ansible_date_time для отображения даты и времени в разных форматах. Чтобы переменная не была пустой, ansible должен собрать факты о хосте. Для этого используем инструкцию gather_facts: yes.

Чтобы увидеть все форматы, напишем такой сценарий:

— name: Ansible fact — ansible_date_time
  debug:
   var: ansible_date_time

Мы должны увидеть что-то на подобие:

ok: [localhost] => {
    «ansible_date_time»: {
        «date»: «2022-11-11»,
        «day»: «11»,
        «epoch»: «1668182701»,
        «hour»: «19»,
        «iso8601»: «2022-11-11T16:05:01Z»,
        «iso8601_basic»: «20221111T190501847269»,
        «iso8601_basic_short»: «20221111T190501»,
        «iso8601_micro»: «2022-11-11T16:05:01.847269Z»,
        «minute»: «05»,
        «month»: «11»,
        «second»: «01»,
        «time»: «19:05:01»,
        «tz»: «MSK»,
        «tz_offset»: «+0300»,
        «weekday»: «Пятница»,
        «weekday_number»: «5»,
        «weeknumber»: «45»,
        «year»: «2022»
    }
}

Чтобы получить только дату, используем:

— name: Get date
  debug:
   var: ansible_date_time.date

8. Генерация случайных строк

Очень полезно, если нам нужно сгенерировать пароль. Выполняется с помощью модулей password и lookup.

Синтаксис следующий:

«{{ lookup(‘password’, ‘<путь до файла, куда сохраняем строку> chars=<через запятую перечисляем разрешенные символы> length=<длина строки>’) }}»

Пример:

— name: Generate new password
  set_fact:
    password_var: «{{ lookup(‘password’, ‘/dev/null chars=ascii_lowercase,ascii_uppercase,digits length=2’) + lookup(‘password’, ‘/dev/null chars=ascii_lowercase,ascii_uppercase,digits,{{ spec_chars }} length=8’) + lookup(‘password’, ‘/dev/null chars=ascii_lowercase,ascii_uppercase,digits length=2’) | replace(‘l’, ‘L’) | replace(‘1’, ‘2’) | replace(‘O’, ‘o’) | replace(‘I’, ‘i’) }}»
  vars:
    spec_chars: ‘!@#$%^&()_-<>{}’

— debug:
    var: password_var

* в данном примере мы получим строку из 12-и символов:

  • первые 2 и последние 2 символа не будут содержать спецсимволов. Я не люблю специальные символы на концах.
  • специальные символы перечислены в переменной spec_chars. Намеренно, исключены некоторые из них, которые могут вызывать проблемы, например звездочка (*).
  • после получения строки мы заменим некоторые символы, которые могу вызывать путаницу. Например, большая буква O похожа на ноль, большая I (и) похожа на маленькую l (эль). И так далее.
  • Результат будет записан в переменную password_var. С помощью модуля debug мы отобразим полученную строку на экране.

О password: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/password_lookup.html.

9. Добавить хост в инвентаризацию.

Ansible проходит по компьютерам согласно списку, который задается в инвентарном файле. Данный список не обязан быть статичным — мы можем добавлять в него хосты с помощью модуля add_host:

— name: Добавим хост mail в группу ‘servers’ с переменной foo
  add_host:
    name: mail
    ansible_ssh_host: 192.168.0.11
    ansible_ssh_port: 22
    groups: servers
    foo: bar

О add_host: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/add_host_module.html.

10. Выполнение действия после отработки задачи.

Предположим, что нам нужно перезапустить сервис, но только в случае, если меняется файл конфигурации. В Ansible предусмотрена опция notify, которая указывает, какую задачу нужно выполнить в случае, если действие привело к измененному состоянию. Например:

— name: Установка расширений PHP
  package:
    name: «{{ item }}»
    state: present
  loop:
    — php-mysql 
    — php-mbstring 
    — php-imap
  notify: restart apache

* в данном примере будет запущена задача restart apache, но только в случае, если на систему будет установлен один из перечисленных пакетов. Если пакеты не пришлось поставить (они уже были установлены), то и notify не отработает.

Важно отметить, что задачи notify необходимо создавать в отдельной секции handlers. Либо в самом файле с заданиями:

Либо в отдельной директории роли — каталоге handlers.

11. Использование бастиона для подключения к хосту по SSH.

У нас может возникнуть необходимость подключиться к серверу, который недоступен по сети напрямую, но к которому можно подключиться через другой компьютер, к которому есть доступ по SSH. Такие узлы принято называть Jump Host или Бастионы.

Для подключения через другой хост необходимо добавить переменную ansible_ssh_common_args со значением ‘-o ProxyCommand=»ssh -p <port> -W %h:%p -q <username>@<bastion.host.net>»‘. Удобнее всего это сделать в инвентарном файле, например:

all:
  hosts:
    my_server:
      ansible_ssh_host: 1.2.3.4
      ansible_ssh_port: 22
      ansible_ssh_common_args: ‘-o ProxyCommand=»ssh -p 22 -W %h:%p -q root@5.6.7.8″‘

* в данном примере мы выполним подключение к хосту 1.2.3.4 через компьютер 5.6.7.8.

Читайте также

Другая информация по Ansible:

1. Инструкция по установке и запуску Ansible на Linux.

2. Примеры ролей Ansible для установки сервисов и настройки системы.

Время на прочтение12 мин

Количество просмотров85K

В этой статье мы рассмотрим плейбуки Ansible — схемы для действий по автоматизации. Плейбуки — это простой, целостный и воспроизводимый способ определить все действия, которые мы хотели бы автоматизировать.

Содержание

  • Что такое плейбук Ansible?

  • Структура плейбука

  • Запуск плейбука

  • Использование переменных в плейбуках

  • Чувствительные данные

  • Запуск задач при изменении с помощью обработчиков

  • Условные задачи

  • Циклы

  • Советы по плейбукам Ansible

Что такое плейбук Ansible?

Плейбуки — это базовые компоненты Ansible, которые записывают и исполняют конфигурацию Ansible. Обычно это основной способ автоматизировать набор задач, которые мы хотели бы выполнять на удалённой машине.

Они собирают все ресурсы, которые нужны, чтобы оркестрировать упорядоченные процессы и не выполнять одни и те же действия вручную. Плейбуки можно использовать повторно и распространять. Их можно легко написать в YAML и так же легко прочитать.

Структура плейбука

Плейбук состоит из сценариев (play), которые выполняются в заданном порядке. Сценарий представляет собой список задач для определённой группы хостов.

Каждая задача связана с модулем, отвечающим за действие, и параметрами конфигурации. Поскольку большинство задач идемпотентны, плейбук можно спокойно запускать несколько раз.

Плейбуки пишутся в YAML со стандартным расширением .yml с минимальным синтаксисом.

Мы делаем одинаковые отступы для элементов на одном уровне иерархии, используя пробелы. У дочернего элемента отступ должен быть больше, чем у родительского. Количество пробелов может быть любым, но обычно их два. Tab использовать нельзя.

Ниже приводится пример простого плейбука с двумя сценариями, по две задачи в каждом:

- name: Example Simple Playbook
  hosts: all
  become: yes

  tasks:
  - name: Copy file example_file to /tmp with permissions
    ansible.builtin.copy:
      src: ./example_file
      dest: /tmp/example_file
      mode: '0644'

  - name: Add the user 'bob' with a specific uid 
    ansible.builtin.user:
      name: bob
      state: present
      uid: 1040

- name: Update postgres servers
  hosts: databases
  become: yes

  tasks:
  - name: Ensure postgres DB is at the latest version
    ansible.builtin.yum:
      name: postgresql
      state: latest

  - name: Ensure that postgresql is started
    ansible.builtin.service:
      name: postgresql
      state: started

Для каждого сценария мы придумываем понятное имя, которое будет указывать на его назначение. Затем мы указываем группу хостов, для которых будет выполняться сценарий, из инвентаря. Наконец, все сценарии должны выполняться от имени root, а для become нужно указать yes.

Для настройки поведения Ansible мы можем определить и другие ключевые слова на разных уровнях — задача, сценарий, плейбук. Более того, большинство ключевых слов можно задать в среде выполнения в виде флагов командной строки в файле конфигурации Ansible, ansible.cfg, или инвентаре. В правилах приоритета прописано, как Ansible ведёт себя в таких случаях.

Затем с помощью параметра tasks мы определяем список задач для каждого сценария. У каждой задачи должно быть понятное имя. Задача выполняет операцию с помощью модуля.

Например, первая задача в первом сценарии использует модуль ansible.builtin.copy. Для модуля мы обычно определяем аргументы. Во второй задаче первого сценария используется модуль ansible.builtin.user для управления учётными записями пользователей. В нашем примере мы настраиваем имя пользователя, состояние пользовательского аккаунта и UID.

Запуск плейбука

При запуске плейбука Ansible выполняет задачи по порядку, по одной за раз, для всех указанных хостов. Это поведение по умолчанию можно скорректировать по необходимости с помощью стратегий.

Если при выполнении задачи происходит сбой, Ansible останавливает выполнение плейбука для этого хоста, но продолжает на других, где задача выполнена успешно. Во время выполнения Ansible отображает информацию о статусе подключения, именах задач, статусе выполнения и наличии изменений.

В конце Ansible предоставляет сводку по выполнению плейбука с указанием выполненных и невыполненных задач. Давайте на примере посмотрим, как это работает. Выполним наш пример плейбука командой ansible-playbook.

В выходных данных мы видим имена сценариев, задачу Gathering Facts (сбор фактов), другие задачи сценария, а в конце Play Recap — сводку по выполнению сценария. Мы не определили группу хостов databases, поэтому второй сценарий плейбука был пропущен.

С помощью флага –limit мы можем ограничить выполнение плейбука несколькими хостами. Например:

ansible-playbook example-simple-playbook.yml --limit host1

Использование переменных в плейбуках

Переменные замещают значения, чтобы мы могли повторно использовать плейбук и другие объекты Ansible. Имя переменной может содержать только буквы, цифры и символы подчёркивания и должно начинаться с буквы.

В Ansible переменные можно определить на нескольких уровнях — см. приоритет переменных. Например, можно задать глобальные переменные для всех хостов, переменные для отдельного хоста или переменные для конкретного сценария.

Для переменных на уровне группы и хоста нужны каталоги group_vars и host_vars. Например, чтобы определить переменные для группы databases, создаём файл group_vars/databases. Дефолтные переменные задаём в файле group_vars/all.

Чтобы определить переменные для определённого хоста, создаём файл с именем этого хоста в каталоге hosts_vars.

Заменить любую переменную в среде выполнения можно с помощью флага -e.

Самый простой метод определить переменные — использовать блок vars в начале сценария со стандартным синтаксисом YAML.

- name: Example Variables Playbook
  hosts: all
  vars:
    username: bob
    version: 1.2.3

Также мы можем определить переменные во внешних файлах YAML.

- name: Example Variables Playbook
  hosts: all
  vars_files:
    - vars/example_variables.yml

Чтобы использовать эти переменные в задачах, мы должны сослаться на них, указав их имя в двойных фигурных скобках, как того требует синтаксис Jinja2:

- name: Example Variables Playbook
  hosts: all
  vars:
    username: bob

  tasks:
  - name: Add the user {{ username }}
    ansible.builtin.user:
      name: "{{ username }}"
      state: present

Если значение переменной начинается с фигурных скобок, нужно взять в кавычки всё выражение, чтобы YAML корректно интерпретировал синтаксис.

Мы также можем определить переменные с несколькими значениями в виде списков.

package:
  - foo1
  - foo2
  - foo3

Мы можем ссылаться на отдельные значения из списка. Например, берём первое значение, foo1:

package: "{{ package[0] }}"

Также переменные можно определить с помощью словарей YAML. Например:

dictionary_example: 
  - foo1: one
  - foo2: two

Здесь тоже можно взять первое поле:

dictionary_example['foo1']

Чтобы ссылаться на вложенные переменные, используем квадратные скобки или точку. Например, нам требуется значение example_name_2:

vars:
  var1:
    foo1:
      field1: example_name_1
      field2: example_name_2

tasks:
- name: Create user for field2 value
  user: 
    name: "{{ var1['foo1']['field2'] }}"

Мы можем создавать переменные с помощью инструкции register, которая получает выходные данные команды или задачи и использует их в других задачах.

- name: Example-2 Variables Playbook
  hosts: all

  tasks:
  - name: Run a script and register the output as a variable
    shell: "find example_file"
    args:
      chdir: "/tmp"
    register: example_script_output

  - name: Use the output variable of the previous task
    debug:
      var: example_script_output

Чувствительные данные

Иногда плейбукам нужны чувствительные данные (ключи API, пароли и т. д.). Для таких случаев у нас есть Ansible Vault. Хранить такие данные обычным текстом небезопасно, поэтому мы можем зашифровывать и расшифровывать их с помощью команды ansible-vault.

Зашифровав секреты и защитив их паролем, мы можем спокойно размещать их в репозитории кода. Ansible Vault защищает данные только при хранении. После расшифровки секретов мы должны обращаться с ними бережно, чтобы случайно не раскрыть.

Шифровать можно переменные или файлы. Зашифрованные переменные расшифровываются по требованию только при необходимости, а зашифрованные файлы расшифровываются всегда, потому что Ansible не знает заранее, понадобится ли их содержимое.

В любом случае, нужно продумать стратегию управления паролями для Vault. Зашифрованное содержимое мы помечаем тегом !vault, который указывает Ansible, что содержимое нужно расшифровать, а перед многострочным зашифрованным фрагментом мы ставим символ |.

Создаем новый зашифрованный файл:

ansible-vault create new_file.yml

Открывается редактор, где можно добавить содержимое, которое должно быть зашифровано. Также можно зашифровать существующие файлы командой encrypt:

ansible-vault encrypt existing_file.yml

Просматриваем зашифрованный файл:

ansible-vault view existing_file.yml

Чтобы внести изменения в зашифрованный файл, временно расшифровываем его командой  edit:

ansible-vault edit existing_file.yml

Изменить пароль от зашифрованного файла можно командой rekey с указанием текущего пароля:

ansible-vault rekey existing_file.yml

Если мы хотим расшифровать файл, мы используем команду decrypt:

ansible-vault decrypt existing_file.yml

С помощью команды encrypt_string можно зашифровать отдельные строки, которые потом можно будет использовать в переменных и включать в плейбуки или файлы переменных:

ansible-vault encrypt_string <password_source> '<string_to_encrypt>' –'<variable_name>'

Например, мы хотим зашифровать строку db_password ‘12345679’ с помощью Ansible Vault:

Поскольку мы опустили <password_source>, мы вручную ввели пароль от Vault. Можно было с тем же успехом передать файл пароля: –vault-password-file.

Чтобы просмотреть содержимое зашифрованной переменной, которую мы сохранили в файле vars.yml, мы используем тот же пароль с флагом –ask-vault-pass:

ansible localhost -m ansible.builtin.debug -a var="db_password" -e "@vars.yml" --ask-vault-pass

Vault password:

localhost | SUCCESS => {
    "changed": false,
    "db_password": "12345678"
}

Чтобы управлять несколькими паролями, можно задать метку с помощью –vault-id. Например, устанавливаем метку dev для файла и запрашиваем пароль:

ansible-vault encrypt existing_file.yml --vault-id dev@prompt

Атрибут no_log: true позволяет запретить вывод на консоль выходных данных задачи, которые могут содержать чувствительные значения:

tasks:
- name: Hide sensitive value example
  debug:
    msg: "This is sensitive information"
  no_log: true

При выполнении задачи на консоль не будет выводиться сообщение:

TASK [Hide sensitive value example] ***********************************
ok: [host1]

Наконец, давайте выполним плейбук с нашей зашифрованной переменной:

Мы убедились, что можем расшифровать значение и использовать его в задачах.

Запуск задач при изменении с помощью обработчиков

Обычно модули Ansible идемпотентны и их можно выполнять много раз, но иногда мы хотим выполнять задачу только при изменении на хосте. Например, мы хотим перезапускать сервис только при изменении его файлов конфигурации.

Ansible использует обработчики, которые срабатывают при уведомлении от других задач. Задачи уведомляют обработчики с помощью параметра notify:, только если они действительно что-то меняют.

У обработчиков должны быть глобально уникальные имена, и обычно мы пишем обработчики в нижней части плейбука.

- name: Example with handler - Update apache config
  hosts: webservers
  
  tasks:
  - name: Update the apache config file
    ansible.builtin.template:
      src: ./httpd.conf
      dest: /etc/httpd.conf
    notify:
    - Restart apache

  handlers:
    - name: Restart apache
      ansible.builtin.service:
        name: httpd
        state: restarted

В примере выше задача Restart apache (перезапустить Apache) будет выполняться, только если в конфигурации что-то изменилось. Обработчики можно считать неактивными задачами, которые ждут уведомления.

Нужно понимать, что по умолчанию обработчики выполняются после завершения всех остальных задач. При таком подходе они выполняются только один раз, даже если триггеров несколько.

Это поведение можно изменить с помощью задачи meta: flush_handlers, которая будет запускать обработчики, уже получившие уведомления на этот момент.

Одна задача может уведомлять несколько разработчиков одной инструкцией notify.

Условные задачи

Условные конструкции — это ещё один способ контролировать порядок выполнения в Ansible. С их помощью мы можем выполнять или пропускать задачи в зависимости от соблюдения условий. Эти условия могут быть связаны с переменными, фактами или результатами предыдущих задач, а также операторами.

Например, мы можем обновлять переменную на основе значения другой переменной, пропускать задачу, если переменная имеет определённое значение, выполнять задачу, только если факт с хоста возвращает значение, превышающее заданный порог.

Чтобы применить простую условную инструкцию, мы указываем параметр when в задаче. Если условие удовлетворяется, задача выполняется. В противном случае — пропускается.

- name: Example Simple Conditional
  hosts: all
  vars:
    trigger_task: true

  tasks:
  - name: Install nginx
    apt:
      name: "nginx"
      state: present
    when: trigger_task

В этом примере задача выполняется, потому что условие удовлетворено.

Кроме того, часто управлять задачами можно на основе атрибутов удалённого хоста, которые можно получить из фактов. Посмотрите список с часто используемыми фактами, чтобы получить представление обо всех фактах, которые можно использовать в условиях.

- name: Example Facts Conditionals 
  hosts: all
  vars:
    supported_os:
      - RedHat
      - Fedora

  tasks:
  - name: Install nginx
    yum:
      name: "nginx"
      state: present
    when: ansible_facts['distribution'] in supported_os

Мы можем сочетать несколько условий с помощью логических операторов и группировать их с использованием скобок:

when: (colour=="green" or colour=="red") and (size="small" or size="medium")

Инструкция when поддерживает использование списка в случаях, когда требуется соблюдение нескольких условий:

when:
  - ansible_facts['distribution'] == "Ubuntu"
  - ansible_facts['distribution_version'] == "20.04"
  - ansible_facts['distribution_release'] == "bionic"

Также можно использовать условия на основе зарегистрированных переменных, которые мы определили в предыдущих задачах:

- name: Example Registered Variables Conditionals
  hosts: all

  tasks:
  - name: Register an example variable
    ansible.builtin.shell: cat /etc/hosts
    register: hosts_contents

  - name: Check if hosts file contains "localhost"
    ansible.builtin.shell: echo "/etc/hosts contains localhost"
    when: hosts_contents.stdout.find(localhost) != -1

Циклы

Ansible позволяет повторять набор элементов в задаче, чтобы выполнять её несколько раз с разными параметрами, при этом не переписывая её. Например, чтобы создать несколько файлов, можно использовать задачу, которая проходит по списку имён каталога, вместо того чтобы писать пять задач с одним модулем.

Для итерации по простому списку элементов указываем ключевое слово loop. Мы можем ссылаться на текущее значение с помощью переменной среды item.

- name: "Create some files"
  ansible.builtin.file:
    state: touch
    path: /tmp/{{ item }}
  loop:
    - example_file1
    - example_file2
    - example_file3

Выходные данные этой задачи, которая использует loop и item:

TASK [Create some files] *********************************
changed: [host1] => (item=example_file1)
changed: [host1] => (item=example_file2)
changed: [host1] => (item=example_file3)

Также возможна итерация по словарям:

- name: "Create some files with dictionaries"
  ansible.builtin.file:
    state: touch
    path: "/tmp/{{ item.filename }}"
    mode: "{{ item.mode }}"
  loop:
    - { filename: 'example_file1', mode: '755'}
    - { filename: 'example_file2', mode: '775'}
    - { filename: 'example_file3', mode: '777'}

Также можно выполнять итерацию по группе хостов в инвентаре:

- name: Show all the hosts in the inventory
  ansible.builtin.debug:
    msg: "{{ item }}"
  loop: "{{ groups['databases'] }}"

Сочетая условные конструкции с циклами, мы можем выполнять задачу только для некоторых элементов в списке:

- name: Execute when values in list are lower than 10
  ansible.builtin.command: echo {{ item }}
  loop: [ 100, 200, 3, 600, 7, 11 ]
  when: item < 10

Наконец, можно использовать ключевое слово until, чтобы выполнять задачу повторно, пока условие не будет удовлетворяться.

- name: Retry a task until we find the word "success" in the logs
  shell: cat /var/log/example_log
  register: logoutput
  until: logoutput.stdout.find("success") != -1
  retries: 10
  delay: 15

В приведённом выше примере мы проверяем файл example_log 10 раз с задержкой в 15 секунд между проверками, пока не найдём слово success. Если мы добавим слово success в файл example_log, пока задача выполняется, через некоторое время мы увидим, что задача успешно останавливается.

TASK [Retry a task until we find the word “success” in the logs] *********
FAILED - RETRYING: Retry a task until we find the word "success" in the logs (10 retries left).
FAILED - RETRYING: Retry a task until we find the word "success" in the logs (9 retries left).
changed: [host1]

Более сложные варианты использования см. в официальном руководстве Ansible по циклам.

Советы по плейбукам Ansible

Придерживайтесь этих рекомендаций при создании плейбуков, чтобы работать продуктивнее.

1. Чем проще, тем лучше.

Не усложняйте задачу. В Ansible есть много вариантов и вложенных структур, и если мы будем бездумно сочетать функции друг с другом, мы получим слишком сложную структуру. Не пожалейте времени на то, чтобы упростить артефакты Ansible. В долгосрочной перспективе это обязательно окупится.

2. Размещайте артефакты Ansible в системе управления версиями.

Плейбук рекомендуется хранить в git или другой системе управления версиями.

3. Создавайте понятные имена для задач, сценариев и плейбуков.

Придумывайте имена, по которым будет сразу понятно, что делает артефакт.

4. Стремитесь к удобочитаемости.

Следите за отступами и добавляйте пустые строки между задачами, чтобы код было проще читать.

5. Всегда явно упоминайте состояние задачи.

У многих модулей есть дефолтное состояние, что позволяет нам пропускать параметр состояния, но лучше явно указывать это значение, чтобы избежать недоразумений.

6. Написание комментариев при необходимости.

Иногда определения задания будет недостаточно, чтобы объяснить всю ситуацию, так что используйте комментарии для сложных частей в плейбуке.

Заключение

В этой статье мы рассмотрели основной компонент автоматизации в Ansible — плейбуки. Мы узнали, как создавать, структурировать и запускать плейбуки.

Мы также рассмотрели использование переменных, защиту чувствительных данных, контроль выполнения задач с помощью обработчиков и условий, а также итерацию по задачам с помощью циклов.

Кстати, если вы хотите использовать модель «инфраструктура как код», попробуйте Spacelift. Он поддерживает рабочие процессы Git, политику как код, программируемую конфигурацию, совместное использование контекста и многие другие удобные функции. Сейчас эта платформа работает с Terraform, Pulumi и CloudFormation с поддержкой Ansible. Здесь можно создать аккаунт, чтобы получить бесплатную пробную версию.

А если хотите глубже изучить Ansible, приходите на курс Ansible: Infrastructure as Code

Вы научитесь конфигурировать рутинные задачи и никакие правки конфигураций вас не остановят. Будете не просто конфигурировать, но и делать это с помощью удобного и простого инструмента. Сможете выполнять сложные задачи, настраивать под свои задачи и смело залезать под капот Ansible. Пойметё, когда и как писать свои модули.

Курс состоит не только из теории, но и из опыта спикера, его набитых шишек, кейсов, а также 78 тестовых и 46 практических заданий на стендах в личном кабинете.

Коротко о программе:

— Узнаете как работать с переменными, как писать плейбуки и роли;

— Развернете LEMP стек, PostgreSQL и Mongo кластеры, задеплоите Flask приложение;

— Напишите свой модуль для Ansible;

— Настроите IaC в Gitlab;

— Разберетесь с работой с облаками и enterprise решениями.

Посмотреть подробную программу и записаться: https://slurm.io/ansible

Исходные данные

Дано:

  • конвейер CI/CD, реализованный, к примеру, в GitLab. Для корректной работы ему требуются, как это очень часто бывает, некие секреты — API-токены, пары логи/пароль, приватные SSH-ключи — да всё, о чём только можно подумать;

  • работает этот сборочный конвейер, как это тоже часто бывает, на базе контейнеров. Соответственно, чем меньше по размеру образы — тем лучше, чем меньше в них всякой всячины — тем лучше.

Требуется консольная утилита, которая:

  • занимает минимум места;

  • умеет расшифровывать секреты, зашифрованные ansible-vault;

  • не требует никаких внешних зависимостей;

  • умеет читать ключ из файла.

Я думаю, что люди, причастные к созданию сборочных конвейеров, по достоинству смогут оценить каждое из этих требований. Ну а что у меня получилось в результате — читайте далее.

На всякий случай сразу напоминаю, что по действующему законодательству разработка средств криптографической защиты информации в РФ — лицензируемая деятельность. Иначе говоря, без наличия лицензии вы не можете просто так взять и продавать получившееся решение.

По поводу допустимости полных текстов расшифровщиков в статьях вроде этой — надеюсь, что компетентные в этом вопросе читатели смогут внести свои уточнения в комментариях.

Начнём сначала

Итак, предположим, что у нас на Linux-хосте с CentOS 7 уже установлен Ansible, к примеру, версии 2.9 для Python версии 3.6. Установлен, конечно же, с помощью virtualenv в каталог «/opt/ansible«. Дальше для целей удовлетворения чистого научного любопытства возьмём какой-нибудь YaML-файл, и зашифруем его с помощью утилиты ansible-vault:

ansible-vault encrypt vaulted.yml --vault-password-file=.password

Этот вызов, как можно догадаться, зашифрует файл vaulted.yml с помощью пароля, который хранится в файле .password.

Итак, что получается после зашифровывания файла с помощью утилиты ansible-vault? На первый взгляд — белиберда какая-то, поэтому спрячу её под спойлер:

Содержимое файла vaulted.yml

$ANSIBLE_VAULT;1.1;AES256
61373536353963313739366536643661313861663266373130373730666634343337356536333664
3365393033623439356364663537353365386464623836640a356464633264626330383232353362
63613135373638393665663962303530323061376432333931306161303966633338303565666337
6465393837636665300a633732313730626265636538363339383237306264633830653665343639
30353863633137313866393566643661323536633666343837623130363966613363373962343630
34386234633236363363326436666630643937313630346230386538613735366431363934316364
37346337323833333165386534353432386663343465333836643131643237313262386634396534
38316630356530626430316238383364376561393637363262613666373836346262666536613164
66316638343162626631623535323666643863303231396432666365626536393062386531623165
63613934323836303536613532623864303839313038336232616134626433353166383837643165
643439363835643731316238316439633039

Ну а как именно эта белиберда работает «под капотом» — давайте разбираться.

Открываем файл /opt/ansible/lib/python3.6/site-packages/ansible/parsing/vault/__init__.py, и в коде метода encrypt класса VaultLib видим следующий вызов:

VaultLib.encrypt

 ...
 b_ciphertext = this_cipher.encrypt(b_plaintext, secret)
 ...

То есть результирующее содержимое нашего файла будет создано в результате вызова метода encrypt некоторого класса. Какого именно — в общем-то, невелика загадка, ниже по файлу есть всего один класс с именем VaultAES256.

Смотрим в его метод encrypt:

VaultAES256.encrypt

@classmethod
def encrypt(cls, b_plaintext, secret):
    if secret is None:
        raise AnsibleVaultError('The secret passed to encrypt() was None')
    b_salt = os.urandom(32)
    b_password = secret.bytes
    b_key1, b_key2, b_iv = cls._gen_key_initctr(b_password, b_salt)

    if HAS_CRYPTOGRAPHY:
        b_hmac, b_ciphertext = cls._encrypt_cryptography(b_plaintext, b_key1, b_key2, b_iv)
    elif HAS_PYCRYPTO:
        b_hmac, b_ciphertext = cls._encrypt_pycrypto(b_plaintext, b_key1, b_key2, b_iv)
    else:
        raise AnsibleError(NEED_CRYPTO_LIBRARY + '(Detected in encrypt)')

    b_vaulttext = b'\n'.join([hexlify(b_salt), b_hmac, b_ciphertext])
    # Unnecessary but getting rid of it is a backwards incompatible vault
    # format change
    b_vaulttext = hexlify(b_vaulttext)
    return b_vaulttext

То есть перво-наперво генерируется «соль» длиной 32 байта. Затем из побайтного представления пароля и «соли» вызовом _gen_key_initctr генерируется пара ключей (b_key1, b_key2) и вектор инициализации (b_iv).

Генерация ключей

Что же происходит в _gen_key_initctr?

_gen_key_initctr:

@classmethod
def _gen_key_initctr(cls, b_password, b_salt):
    # 16 for AES 128, 32 for AES256
    key_length = 32

    if HAS_CRYPTOGRAPHY:
        # AES is a 128-bit block cipher, so IVs and counter nonces are 16 bytes
        iv_length = algorithms.AES.block_size // 8

        b_derivedkey = cls._create_key_cryptography(b_password, b_salt, key_length, iv_length)
        b_iv = b_derivedkey[(key_length * 2):(key_length * 2) + iv_length]
    elif HAS_PYCRYPTO:
        # match the size used for counter.new to avoid extra work
        iv_length = 16

        b_derivedkey = cls._create_key_pycrypto(b_password, b_salt, key_length, iv_length)
        b_iv = hexlify(b_derivedkey[(key_length * 2):(key_length * 2) + iv_length])
    else:
        raise AnsibleError(NEED_CRYPTO_LIBRARY + '(Detected in initctr)')

    b_key1 = b_derivedkey[:key_length]
    b_key2 = b_derivedkey[key_length:(key_length * 2)]

    return b_key1, b_key2, b_iv

Если по сути, то внутри этого метода вызов _create_key_cryptography на основе пароля, «соли», длины ключа и длины вектора инициализации генерирует некий производный ключ (строка 10 приведённого фрагмента). Далее этот производный ключ разбивается на части, и получаются те самые b_key1, b_key2 и b_iv.

Следуем по кроличьей норе дальше. Что внутри _create_key_cryptography?

_create_key_cryptography:

@staticmethod
def _create_key_cryptography(b_password, b_salt, key_length, iv_length):
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(),
        length=2 * key_length + iv_length,
        salt=b_salt,
        iterations=10000,
        backend=CRYPTOGRAPHY_BACKEND)
    b_derivedkey = kdf.derive(b_password)

    return b_derivedkey

Ничего особенного. Если оставить в стороне всю мишуру, то в итоге вызывается функция библиотеки OpenSSL под названием PBKDF2HMAC с нужными параметрами. Можете, кстати, самолично в этом убедиться, открыв файл /opt/ansible/lib/python3.6/site-packages/cryptography/hazmat/backends/openssl/backend.py.

Кстати, длина производного ключа, как видите, специально выбирается таким образом, чтобы хватило и на b_key1, и на b_key2, и на b_iv.

Собственно шифрование

Движемся дальше. Здесь нас встречает вызов _encrypt_cryptography с параметрами в виде открытого текста, обоих ключей и вектора инициализации:

_encrypt_cryptography

@staticmethod
def _encrypt_cryptography(b_plaintext, b_key1, b_key2, b_iv):
    cipher = C_Cipher(algorithms.AES(b_key1), modes.CTR(b_iv), CRYPTOGRAPHY_BACKEND)
    encryptor = cipher.encryptor()
    padder = padding.PKCS7(algorithms.AES.block_size).padder()
    b_ciphertext = encryptor.update(padder.update(b_plaintext) + padder.finalize())
    b_ciphertext += encryptor.finalize()

    # COMBINE SALT, DIGEST AND DATA
    hmac = HMAC(b_key2, hashes.SHA256(), CRYPTOGRAPHY_BACKEND)
    hmac.update(b_ciphertext)
    b_hmac = hmac.finalize()

    return to_bytes(hexlify(b_hmac), errors='surrogate_or_strict'), hexlify(b_ciphertext)

В принципе, тут нет ничего особенного: шифр инициализируется из вектора b_iv, затем первым ключом b_key1 шифруется исходный текст, а результат этого шифрования хэшируется с помощью второго ключа b_key2.

Полученные в итоге байты подписи и шифртекста преобразуются в строки своих шестнадцатеричных представлений через hexlify. (см. строка 14 фрагмента выше)

Окончательное оформление файла

Возвращаемся к строкам 16-20 фрагмента VaultAES256.encrypt: три строки, содержащие «соль», подпись и шифртекст, склеиваются вместе, после чего снова преобразуются в шестнадцатеричное представление (комментарий прямо подсказывает, что это — для обратной совместимости).

Дальше дописывается заголовок (помните, тот самый — $ANSIBLE_VAULT;1.1;AES256), ну и, в общем-то, всё.

Обратный процесс

После того, как мы разобрались в прямом процессе, реализовать обратный будет не слишком сложно — по крайней мере, если выбрать правильный инструмент.

Понятно, что Python нам не подходит, иначе можно было и огород не городить: ansible-vault одинаково хорошо работает в обе стороны. С другой стороны, никто не мешает на базе библиотек Ansible написать что-либо своё — в качестве разминки перед «подходом к снаряду» я так и сделал, и о результате напишу отдельную статью.

Тем не менее, для написания предмета статьи я воспользовался FreePascal. Ввиду того, что языковой холивар темой статьи не является, буду краток: выбрал этот язык, во-первых, потому что могу, а во-вторых — потому что получаемый бинарник удовлетворяет заданным требованиям.

Итак, нам понадобятся: FreePascal версии 3.0.4 (эта версия в виде готовых пакетов — самая свежая, нормально устанавливающаяся в CentOS 7), и библиотека DCPCrypt версии 2.1 (на GitHub). Интересно, что прямо вместе с компилятором (fpc) и обширным набором библиотек в rpm-пакете поставляется консольная среда разработки fp.

К сожалению, «искаропки» модули этой библиотеки не собираются компилятором fpc — в них нужны минимальные правки. С другой стороны, я предполагаю, что без этих правок предмет статьи перестаёт относиться к лицензируемым видам деятельности и начинает представлять чисто академический интерес — именно поэтому выкладываю статью без них.

Часть кода, относящуюся к генерированию производного ключа (реализацию той самой функции PBKDF2), я нашёл в интернете, и поместил в отдельный модуль под названием «kdf».

Вот этот модуль собственной персоной:

kdf.pas

{$MODE OBJFPC}

// ALL CREDITS FOR THIS CODE TO https://keit.co/p/dcpcrypt-hmac-rfc2104/

unit kdf;

interface
uses dcpcrypt2,math;
function PBKDF2(pass, salt: ansistring; count, kLen: Integer; hash: TDCP_hashclass): ansistring;
function CalcHMAC(message, key: string; hash: TDCP_hashclass): string;

implementation
function RPad(x: string; c: Char; s: Integer): string;
var
  i: Integer;
begin
  Result := x;
  if Length(x) < s then
    for i := 1 to s-Length(x) do
      Result := Result + c;
end;

function XorBlock(s, x: ansistring): ansistring; inline;
var
  i: Integer;
begin
  SetLength(Result, Length(s));
  for i := 1 to Length(s) do
    Result[i] := Char(Byte(s[i]) xor Byte(x[i]));
end;

function CalcDigest(text: string; dig: TDCP_hashclass): string;
var
  x: TDCP_hash;
begin
  x := dig.Create(nil);
  try
    x.Init;
    x.UpdateStr(text);
    SetLength(Result, x.GetHashSize div 8);
    x.Final(Result[1]);
  finally
    x.Free;
  end;
end;

function CalcHMAC(message, key: string; hash: TDCP_hashclass): string;
const
  blocksize = 64;
begin
  // Definition RFC 2104
  if Length(key) > blocksize then
    key := CalcDigest(key, hash);
  key := RPad(key, #0, blocksize);
  Result := CalcDigest(XorBlock(key, RPad('', #$36, blocksize)) + message, hash);
  Result := CalcDigest(XorBlock(key, RPad('', #$5c, blocksize)) + result, hash);
end;

function PBKDF1(pass, salt: ansistring; count: Integer; hash: TDCP_hashclass): ansistring;
var
  i: Integer;
begin
  Result := pass+salt;
  for i := 0 to count-1 do
    Result := CalcDigest(Result, hash);
end;

function PBKDF2(pass, salt: ansistring; count, kLen: Integer; hash: TDCP_hashclass): ansistring;

  function IntX(i: Integer): ansistring; inline;
  begin
    Result := Char(i shr 24) + Char(i shr 16) + Char(i shr 8) + Char(i);
  end;

var
  D, I, J: Integer;
  T, F, U: ansistring;
begin
  T := '';
  D := Ceil(kLen / (hash.GetHashSize div 8));
  for i := 1 to D do
  begin
    F := CalcHMAC(salt + IntX(i), pass, hash);
    U := F;
    for j := 2 to count do
    begin
      U := CalcHMAC(U, pass, hash);
      F := XorBlock(F, U);
    end;
    T := T + F;
  end;
  Result := Copy(T, 1, kLen);
end;

end.

Из бросающегося в глаза — обратите внимание, что в Pascal и его потомках отсутствует классическое разделение на заголовочные файлы и файлы собственно с кодом, в этом смысле модульная организация роднит его с Python, и отличает от C.

Однако от питонячьего модуля паскалевский отличается ещё и тем, что «снаружи» доступны только те функции/переменные, которые объявлены в секции interface. То есть по умолчанию внутри модуля ты можешь хоть «на ушах стоять» — снаружи никто не сможет вызвать твои внутренние API. Так устроен язык, а хорошо это или плохо — вопрос вкуса, поэтому оценки оставим в стороне (питонистам передают привет функции/методы, начинающиеся на «_» и «__»).

Заголовочная часть

Код, как обычно, под спойлером.

Заголовочная часть («шапка», header)

program devault;
uses
  math, sysutils, strutils, getopts, DCPcrypt2, DCPsha256, DCPrijndael, kdf;

Далее нам понадобится пара функций — hexlify и unhexlify (набросаны, конечно, «на скорую руку»). Они являются аналогами соответствующих функций Python — вторая возвращает строку из шестнадцатеричных представлений байтов входного аргумента, а первая — наоборот, переводит строку шестнадцатеричных кодов обратно в байты.

hexlify/unhexlify

function unhexlify(s:AnsiString):AnsiString;
var i:integer;
    tmpstr:AnsiString;
begin
  tmpstr:='';
  for i:=0 to (length(s) div 2)-1 do
    tmpstr:=tmpstr+char(Hex2Dec(Copy(s,i*2+1,2)));
  unhexlify:=tmpstr;
end;

function hexlify(s:AnsiString):AnsiString;
var i:integer;
    tmpstr:AnsiString;
begin
  tmpstr:='';
  for i:=1 to (length(s)) do
    tmpstr:=tmpstr+IntToHex(ord(s[i]),2);
  hexlify:=tmpstr;
end;

Назначение функций showbanner(), showlicense() и showhelp() очевидно из названий, поэтому я просто приведу их без комментариев.

showbanner() / showlicense() / showhelp()

showbanner()

procedure showbanner();
begin
  WriteLn(stderr, 'DeVault v1.0');
  Writeln(stderr, '(C) 2021, Sergey Pechenko. All rights reserved');
  Writeln(stderr, 'Run with "-l" option to see license');
end;

showlicense()

procedure showlicense();
begin
  WriteLn(stderr,'Redistribution and use in source and binary forms, with or without modification,');
  WriteLn(stderr,'are permitted provided that the following conditions are met:');
  WriteLn(stderr,'* Redistributions of source code must retain the above copyright notice, this');
  WriteLn(stderr,'   list of conditions and the following disclaimer;');
  WriteLn(stderr,'* Redistributions in binary form must reproduce the above copyright notice, ');
  WriteLn(stderr,'   this list of conditions and the following disclaimer in the documentation');
  WriteLn(stderr,'   and/or other materials provided with the distribution.');
  WriteLn(stderr,'* Sergey Pechenko''s name may not be used to endorse or promote products');
  WriteLn(stderr,'   derived from this software without specific prior written permission.');
  WriteLn(stderr,'THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"');
  WriteLn(stderr,'AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,');
  WriteLn(stderr,'THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE');
  WriteLn(stderr,'ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE');
  WriteLn(stderr,'FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES');
  WriteLn(stderr,'(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;');
  WriteLn(stderr,'LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON');
  WriteLn(stderr,'ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT');
  WriteLn(stderr,'(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,');
  WriteLn(stderr,'EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.');
  WriteLn(stderr,'Commercial license can be obtained from author');
end;

showhelp()

procedure showhelp();
begin
  WriteLn(stderr,'Usage:');
  WriteLn(stderr,Format('%s <-p password | -w vault_password_file> [-f secret_file]',[ParamStr(0)]));
  WriteLn(stderr,#09'"password" is a text string which was used to encrypt your secured content');
  WriteLn(stderr,#09'"vault_password_file" is a file with password');
  WriteLn(stderr,#09'"secret_file" is a file with encrypted content');
  WriteLn(stderr,'When "-f" argument is absent, stdin is read by default');
end;

Дальше объявляем переменные и константы, которые будут использоваться в коде. Привожу их здесь только для полноты текста, потому что комментировать тут особо нечего.

Переменные и константы

var secretfile, passwordfile, pass, salt, b_derived_key, b_key1, b_key2, b_iv,
    hmac_new, cphrtxt, fullfile, header, tmpstr, hmac:Ansistring;
    Cipher: TDCP_rijndael;
    key, vector, data, crypt: RawByteString;
    fulllist: TStringArray;
    F: Text;
    c: char;
    opt_idx: LongInt;
    options: array of TOption;
const KEYLENGTH=32; // for AES256
const IV_LENGTH=128 div 8;
const CONST_HEADER='$ANSIBLE_VAULT;1.1;AES256';

Код

Ну, почти код — всё ещё вспомогательная функция, которая в рантайме готовит массив записей для разбора параметров командной строки. Почему она здесь — потому что работает с переменными, объявленными в секции vars выше.

preparecliparams()

procedure preparecliparams();
begin
  SetLength(options, 6);
  with options[1] do
    begin
      name:='password';
      has_arg:=Required_Argument;
      flag:=nil;
      value:=#0;
    end;
  with options[2] do
    begin
      name:='file';
      has_arg:=Required_Argument;
      flag:=nil;
      value:=#0;
    end;
  with options[3] do
    begin
      name:='passwordfile';
      has_arg:=Required_Argument;
      flag:=nil;
      value:=#0;
    end;
  with options[4] do
    begin
      name:='version';
      has_arg:=No_Argument;
      flag:=nil;
      value:=#0;
    end;
  with options[5] do
    begin
      name:='license';
      has_arg:=No_Argument;
      flag:=nil;
      value:=#0;
    end;
  with options[6] do
    begin
      name:='help';
      has_arg:=No_Argument;
      flag:=nil;
      value:=#0;
    end;
end;

А вот теперь точно код самой утилиты:

Весь остальной код

begin
  repeat
    c:=getlongopts('p:f:w:lh?',@options[1],opt_idx);
    case c of
      'h','?' : begin showhelp(); halt(0); end;
      'p' : pass:=optarg;
      'f' : secretfile:=optarg;
      'w' : passwordfile:=optarg;
      'v' : begin showbanner(); halt(0); end;
      'l' : begin showlicense(); halt(0); end;
      ':' : writeln ('Error with opt : ',optopt); // not a mistake - defined in getops unit
     end;
  until c=endofoptions;
  if pass = '' then // option -p not set
    if passwordfile <> '' then
      try
        Assign(F,passwordfile);
        Reset(F);
        Readln(F,pass);
        Close(F);
      except
        on E: EInOutError do
        begin
          Close(F);
          writeln(stderr, 'Password not set and password file cannot be read, exiting');
          halt(1);
        end;
      end
    else
      begin // options -p and -w are both not set
          writeln(stderr, 'Password not set, password file not set, exiting');
          showhelp();
          halt(1);
      end;
  try
    Assign(F,secretfile);
    Reset(F);
  except
    on E: EInOutError do
    begin
      writeln(stderr, Format('File %s not found, exiting',[secretfile]));
      halt(1);
    end;
  end;
  readln(F,header);
  if header<>CONST_HEADER then
    begin
      writeln(stderr, 'Header mismatch');
      halt(1);
    end;
  fullfile:='';
  while not EOF(F) do
    begin
    Readln(F,tmpstr);
    fullfile:=fullfile+tmpstr;
    end;
  Close(F);
  fulllist:=unhexlify(fullfile).Split([#10],3);
  salt:=fulllist[0];
  hmac:=fulllist[1];
  cphrtxt:=fulllist[2];
  salt:=unhexlify(salt);
  cphrtxt:=unhexlify(cphrtxt);
  b_derived_key:=PBKDF2(pass, salt, 10000, 2*32+16, TDCP_sha256);
  b_key1:=Copy(b_derived_key,1,KEYLENGTH);
  b_key2:=Copy(b_derived_key,KEYLENGTH+1,KEYLENGTH);
  b_iv:=Copy(b_derived_key,KEYLENGTH*2+1,IV_LENGTH);
  hmac_new:=lowercase(hexlify(CalcHMAC(cphrtxt, b_key2, TDCP_sha256)));
  if hmac_new<>hmac then
    begin
    writeln(stderr, 'Digest mismatch - file has been tampered with, or an error has occured');
    Halt(1);
    end;
  SetLength(data, Length(crypt));
  Cipher := TDCP_rijndael.Create(nil);
  try
    Cipher.Init(b_key1[1], 256, @b_iv[1]);
    Cipher.DecryptCTR(cphrtxt[1], data[1], Length(data));
    Cipher.Burn;
  finally
    Cipher.Free;
  end;
  Writeln(data);
end.

Дальше будет странная таблица, но, кажется, это — самый удобный способ рассказа об исходном коде.

Стр.

Назначение

2-13

разбор параметров командной строки с отображением нужных сообщений;

14-34

проверка наличия пароля в параметрах, при отсутствии — попытка прочесть пароль из файла, при невозможности — останавливаем работу;

35-44

попытка прочесть зашифрованный файл, указанный в параметрах;

Небольшой чит: по умолчанию имя файла (переменная secretfile) равно пустой строке; в этом случае вызов Assign(F, secretfile) в строке 36 свяжет переменную F с stdin

45-50

проверка наличия в файле того самого заголовка $ANSIBLE_VAULT;1.1;AES256;

51-57

читаем всё содержимое зашифрованного файла и закрываем его;

58-63

разбираем файл на части: «соль», дайджест, шифртекст — всё отдельно; при этом все три части нужно будет ещё раз прогнать через unhexlify (помните примечание в VaultAES256.encrypt?)

64-73

вычисление производного ключевого материала; разбиение его на части; расчёт дайджеста; проверка зашифрованного файла на корректность дайждеста;

74-83

подготовка буфера для расшифрованного текста; расшифровка; затирание ключей в памяти случайными данными; вывод расшифрованного содержимого в поток stdout

Интересная информация для питонистов

Кстати, вы же слышали, что в Python 3.10 наконец-то завезли оператор case (PEP-634)? Интересно, что его ввёл сам BDFL, и произошло это примерно через 14 лет после того, как по результатам опроса на PyCon 2007 первоначальный PEP-3103 был отвергнут.

Собственно, теперь всё на месте, осталось собрать:

[root@ansible devault]# time fpc devault.pas -Fudcpcrypt_2.1:dcpcrypt_2.1/Ciphers:dcpcrypt_2.1/Hashes -MOBJFPC

Здесь имейте в виду, что форматирование Хабра играет злую шутку — никакого разрыва строки после первого минуса нет.

Вывод компилятора

Free Pascal Compiler version 3.0.4 [2017/10/02] for x86_64
Copyright (c) 1993-2017 by Florian Klaempfl and others
Target OS: Linux for x86-64
Compiling devault.pas
Compiling ./dcpcrypt_2.1/DCPcrypt2.pas
Compiling ./dcpcrypt_2.1/DCPbase64.pas
Compiling ./dcpcrypt_2.1/Hashes/DCPsha256.pas
Compiling ./dcpcrypt_2.1/DCPconst.pas
Compiling ./dcpcrypt_2.1/Ciphers/DCPrijndael.pas
Compiling ./dcpcrypt_2.1/DCPblockciphers.pas
Compiling kdf.pas
Linking devault
/usr/bin/ld: warning: link.res contains output sections; did you forget -T?
3784 lines compiled, 0.5 sec

real    0m0.543s
user    0m0.457s
sys     0m0.084s

Вроде неплохо: 3,8 тысячи строк кода собраны до исполняемого файла за 0.6 сек. На выходе — статически связанный бинарник, которому для работы от системы требуется только ядро. Ну то есть для запуска достаточно просто скопировать этот бинарник в файловую систему — и всё. Кстати, я забыл указать его размер: 875К. Никаких зависимостей, компиляций по несколько минут и т.д.

Ах да, чуть не забыл самое интересное! Запускаем, предварительно сложив пароль в файл «.password»:

[root@ansible devault]# ./devault -w .password -f vaulted.yml
---
collections:
- name: community.general
  scm: git
  src: https://github.com/ansible-collections/community.general.git
  version: 1.0.0

Вот такой нехитрый YaML я использовал в самом начале статьи для создания зашифрованного файла.

Исходный код для самостоятельного изучения можно взять здесь.

Хотите ещё Ansible? (осторожно, денежные вопросы!)

Теперь у вас есть возможность сделать пожертвование автору статьи — оно дополнительно замотивирует меня чаще выбираться на прогулку с чашкой кофе и булкой с корицей, чтобы отдохнуть перед написанием следующей статьи и обдумать её содержание.

Если же хотите систематизировать и углубить свои знания Ansible — я провожу тренинги по Ansible, пишите мне в Telegram.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Хотите ещё технической дичи про Ansible?

31.71% Автор, жги! А я пойду в последний спойлер, сделаю пожертвование :-) 13

17.07% В теории — конечно, интересно, но лично для меня этот материал не стоит даже чашки кофе7

0% Не, не интересно. Предложу свою тему в комментариях.0

51.22% Я мимокрокодил, хочу посмотреть результаты21

Проголосовал 41 пользователь. Воздержались 9 пользователей.

Введение

Автоматизация инфраструктуры — не роскошь, а необходимость в современном DevOps-цикле. Конфигурация серверов, установка пакетов, деплой приложений и масштабирование ресурсов вручную не только утомительны, но и подвержены ошибкам. Чтобы устранить хаос и повысить воспроизводимость, в дело вступает Ansible.

Ansible — это open-source-инструмент для автоматизации задач администрирования, написанный на Python. Он не требует установки агентов, использует YAML и SSH, а также имеет простую, читаемую синтаксическую структуру. Его используют компании уровня NASA, Twitter, и Red Hat.

В этом материале — полное погружение в Ansible: теория, структура, методы, функции, плейбуки, примеры и API-интеграции.

Основная часть

Что такое Ansible

Краткое определение

Ansible — это инструмент автоматизации конфигураций, управления серверами, оркестрации и деплоя. Написан на Python и использует YAML как DSL для описания задач.

Преимущества

  • Безагентная архитектура

  • Использует SSH

  • Простота синтаксиса (YAML)

  • Расширяемость через Python-модули

  • Поддержка Windows и Linux

Установка

pip install ansible

Или через пакетный менеджер:

sudo apt install ansible

Архитектура Ansible

Основные компоненты

Компонент Назначение
Inventory Список управляемых хостов
Playbook Набор задач в YAML-файле
Module Отдельная операция (копирование, установка)
Role Переиспользуемая логическая структура задач
Variable Переменные, шаблоны
Plugin Расширение поведения (callback, connection)

Inventory (инвентори)

[web]
web1 ansible_host=192.168.1.10
web2 ansible_host=192.168.1.11

Основы Playbook

Playbook — это сценарий в YAML-формате, описывающий задачи и их порядок.

Пример

- name: Установка Nginx
  hosts: web
  become: true
  tasks:
    - name: Установка пакета
      apt:
        name: nginx
        state: present

Структура

  • name: описание задачи

  • hosts: цель (группа из inventory)

  • tasks: список действий

  • become: использование sudo

Модули Ansible

Модули — единицы логики: копирование файлов, перезапуск сервиса и т.д. Поставляются по умолчанию или пишутся самостоятельно.

Популярные модули

Модуль Назначение
apt Управление пакетами (Debian)
yum Пакеты в RedHat/CentOS
copy Копирование файлов
template Jinja2-шаблоны
service Управление службами
command Выполнение shell-команд

Пример copy

- name: Копирование index.html
  copy:
    src: ./index.html
    dest: /var/www/html/index.html

Переменные и шаблоны

Переменные можно задавать в playbook, в инвентори, через CLI или в vars-файлах. Шаблоны — через Jinja2.

Пример

vars:
  app_port: 8080

Jinja2-шаблон (template.j2)

server {
    listen {{ app_port }};
}

Роли и структуры

Роли позволяют разделить логику на повторно используемые компоненты.

ansible-galaxy init myrole

Создаётся структура:

myrole/
├── tasks/main.yml
├── handlers/main.yml
├── templates/
├── vars/main.yml

В playbook:

roles:
  - myrole

Ansible и Python API

Ansible может использоваться программно через ansible-runner и ansible.inventory.manager.

Пример вызова playbook через Python

import ansible_runner
r = ansible_runner.run(private_data_dir='/tmp/demo', playbook='site.yml')
print(r.status)
print(r.rc)

Работа с инвентарём

from ansible.parsing.dataloader import DataLoader
from ansible.inventory.manager import InventoryManager

loader = DataLoader()
inventory = InventoryManager(loader=loader, sources='hosts.ini')

Ansible Galaxy и коллекции

Ansible Galaxy — хранилище ролей и коллекций.

ansible-galaxy install geerlingguy.mysql

В playbook:

roles:
  - geerlingguy.mysql

CI/CD с Ansible

Ansible интегрируется с Jenkins, GitLab, GitHub Actions.

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Установка Ansible
        run: sudo apt install ansible -y
      - name: Деплой
        run: ansible-playbook -i inventory site.yml

Безопасность

  • Используйте ansible-vault для хранения секретов

  • Применяйте минимальные привилегии

  • Логируйте выполнение

ansible-vault encrypt secrets.yml

Ошибки и отладка

Запуск с подробным выводом:

ansible-playbook -vvv site.yml

Проверка синтаксиса:

ansible-playbook --syntax-check site.yml

Сравнение с другими инструментами

Инструмент Агент Язык Подходит для
Ansible Нет YAML+Python Универсальная DevOps-автоматизация
Puppet Да Puppet DSL Большие инфраструктуры
Chef Да Ruby Сложные сценарии
SaltStack Да YAML Реактивные системы

Основные компоненты Ansible Python API

  1. ansible-core — базовая библиотека для работы с Ansible.

  2. ansible-runner — запуск Ansible playbooks и ad-hoc команд из Python.

  3. ansible.module_utils — утилиты для написания кастомных модулей.

  4. ansible.inventory — управление инвентарём.

  5. ansible.playbook — работа с playbooks.


1. ansible-core — базовые функции

Основные классы и функции

Класс/Функция Описание
ansible.constants Глобальные константы Ansible (настройки).
ansible.errors Исключения Ansible (AnsibleErrorAnsibleParserError).
ansible.executor Исполнение задач (TaskExecutor).
ansible.parsing Парсинг YAML и JSON (DataLoader).
ansible.plugins Загрузка плагинов (PluginLoader).
ansible.template Шаблонизация Jinja2 (Templar).

Пример использования

from ansible.parsing.dataloader import DataLoader
from ansible.inventory.manager import InventoryManager

loader = DataLoader()
inventory = InventoryManager(loader=loader, sources='hosts.ini')
print(inventory.list_hosts())  

2. ansible-runner — запуск Ansible из Python

Библиотека для выполнения playbooks и ad-hoc команд.

Основные функции

Функция Описание
run() Запуск playbook или модуля.
run_async() Асинхронный запуск.
get_inventory() Получение инвентаря.

Пример

import ansible_runner

result = ansible_runner.run(
    inventory='hosts.ini',
    module='ping',
    host_pattern='all'
)
print(result.stats)  

3. ansible.module_utils — создание кастомных модулей

Используется для написания своих модулей Ansible.

Основные модули

Модуль Описание
basic.AnsibleModule Базовый класс для модулей.
common.collections Работа с коллекциями.
common.parameters Парсинг параметров.

Пример модуля

from ansible.module_utils.basic import AnsibleModule

def main():
    module = AnsibleModule(
        argument_spec=dict(
            name=dict(type='str', required=True),
            enabled=dict(type='bool', default=True)
        )
    )
    name = module.params['name']
    module.exit_json(changed=True, msg=f"Hello, {name}!")

if __name__ == '__main__':
    main()

4. ansible.inventory — управление инвентарём

Работа с хостами и группами.

Основные классы

Класс Описание
InventoryManager Управление инвентарём.
Host Описание хоста.
Group Описание группы.

Пример

from ansible.inventory.manager import InventoryManager
from ansible.parsing.dataloader import DataLoader

loader = DataLoader()
inventory = InventoryManager(loader=loader, sources='hosts.ini')

for host in inventory.get_hosts():
    print(host.name, host.vars)

5. ansible.playbook — выполнение playbooks

Основные классы

Класс Описание
PlaybookExecutor Запуск playbook.
Play Отдельный play.
Task Задача в playbook.

Пример

from ansible.playbook import Playbook
from ansible import context

context.CLIARGS = {'inventory': 'hosts.ini'}
pb = Playbook.load('deploy.yml', loader=DataLoader())
pb.run()

6. Дополнительные модули

1. ansible.vars — управление переменными

from ansible.vars.manager import VariableManager

vars = VariableManager(loader=DataLoader(), inventory=inventory)
print(vars.get_vars())

2. ansible.galaxy — работа с коллекциями

from ansible.galaxy.collection import CollectionRequirement

col = CollectionRequirement.from_name('community.general')
col.install()

3. ansible.utils — вспомогательные утилиты

from ansible.utils.display import Display

display = Display()
display.warning("Это предупреждение!")

Пример: запуск playbook из Python

from ansible import context
from ansible.executor.playbook_executor import PlaybookExecutor
from ansible.inventory.manager import InventoryManager
from ansible.parsing.dataloader import DataLoader

loader = DataLoader()
inventory = InventoryManager(loader=loader, sources='hosts.ini')
context.CLIARGS = {'inventory': inventory}

executor = PlaybookExecutor(
    playbooks=['deploy.yml'],
    inventory=inventory,
    loader=loader,
    passwords={}
)
executor.run()

Заключение

Ansible — это мощный, но простой инструмент автоматизации инфраструктуры. Он сочетает читаемость YAML, гибкость Python, мощные модули и бесагентную архитектуру. Для DevOps-инженеров и разработчиков Ansible остаётся одним из лучших выборов для управления конфигурацией, деплоем и масштабируемостью.

Понравилась статья? Поделить с друзьями:
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
  • Нет звука на мак под windows
  • Как создать жесткую ссылку в windows 10
  • Windows vista sp1 english
  • Intel hd graphics 610 driver windows 10 64 bit
  • Как контролировать температуру процессора в windows 10