-
Notifications
You must be signed in to change notification settings - Fork 0
2. Setup
postfix
Copy the example template_info.example.yml
file and edit the information as you see fit.
cp ansible/defaults/template_info.example.yml ansible/defaults/template_info.yml
Copy the example secrets.example.yml
file and add the your cloud provider API key('s).
cp ansible/defaults/secrets.example.yml ansible/defaults/secrets.yml
NOTE: The OCI implementation does not use a simple set of API Keys. The way this works on Oracle's end is a public/private key pair for access to their API. For ease of use the Terraform code for OCI embeded in Anster expects you to setup a OCI config file in ~/.oci/config
. Please refer to the following Oracle link to setup this config file. Essentially when you generate/setup an API Key on the Oracle Cloud platform with a private/public key pair, they will give you a example config matching your account settings. Copy paste that content into ~/.osi/config
and update the private key path in that file (should be the last line) to your key you setup for authentication towards OCI. The Terraform OCI provider in Anster is setup in such a way it will automatically pick up this file and do it's magic without needing you to define any variables. The current implementation however does expect there to by only a single profile in that config file. If you define more I'm pretty sure Anster will break on this.
Now you can run:
-
ansible-playbook ansible/main.yml --tags=create
to create the infrastructure, install ansible roles locally, bootstrap the created servers and apply your task (if any are defined inansible/tasks/main.yml
). -
ansible-playbook ansible/main.yml
to only apply your task to the server (if any are defined inansible/tasks/main.yml
). -
ansible-playbook ansible/main.yml --tags=destroy
to destroy the infrastructure deployed by terraform.
Other additional tags are:
-
ansible-playbook ansible/main.yml --tags=tfvars
to only create theterraform.tfvars
file. Useful if you want to work on the Terraform code. -
ansible-playbook ansible/main.yml --tags=roles
to only instal ansible roles locally. -
ansible-playbook ansible/main.yml --tags=bootstrap
to only run the bootstrap tasks.
NOTE: The template is smart enough to know if you made changes to the infrastructure variable (host_list
). It only keeps VMs on cloud providers that are currently configured in the host_list
variable. VMs that are no longer listed in the host_list
variable are automatically destroyed and new ones will automatically be created. It will however not configure the new servers with the bootstrap playbook if you did not supply the create
or the bootstrap
tag on the ansible-playbook
command.
To avoid having clear text API tokens linger on disk you can encrypt the ansible/defaults/secrets.yml
with ansible-vault
. The following example will allow you to setup a passphrase before you are able to use ansible/defaults/secrets.yml
.
ansible-vault encrypt ansible/defaults/secrets.yml
Then from this point onward I would recommend you add the --ask-vault-pass
flag to the ansible commands.
$ ansible-playbook ansible/main.yml --ask-vault-pass --tags=create
Vault password:
If you ever need to edit the API tokens in ansible/defaults/secrets.yml
you can do so with ansible-vault edit
.
ansible-vault edit ansible/defaults/secrets.yml
For more information about ansible-vault click here.
If you require additional tasks to be ran after initial configuration you can add those directly into ansible/main.yml
or split them out to separate tasks files such as ansible/tasks/main.yml
.
NOTE: Make sure you add a when
-statement that prevents other tasks from running. See the examples in setting up the host_list.
If your tasks require additional roles you can have them be automatically installed by adding them to the ansible/files/requirements.yml
file. This will install these roles to the ansible/roles
folder.
If you want to setup variables for your tasks add these to the ansible/defaults/main.yml
file.
By setting up a host_list
variable in the template_info.yml
-file you can define the following things:
- How many hosts should be deployed.
- How a host should be deployed.
- Where the host should be deployed.
- To what ansible inventory group the host should be added.
The example below defines one host on digitalocean. In this case we want to deploy a simple webserver.
host_list: {
digitalocean: [
{ "name": "web01",
"tag": "[\"web\"]"
}
]
}
Since not all terraform values are defined for the VM in the host_list
variable, the missing values will use their default settings as show below:
locals {
digitalocean_servers = defaults(var.digitalocean_servers, {
size = "s-1vcpu-1gb"
image = "ubuntu-20-04-x64"
region = "ams3"
backups = false
monitoring = false
ipv6 = false
resize_disk = true
droplet_agent = false
create_vpc = true
})
}
NOTE: Terraform also adds tags/lables to the deployed hosts on the cloud provider. In this case we added a tag to web01
called web
. These tags/lables are used by Ansible to add the specific host to an inventory groups that matches the tag/label. Meaning you can easily add host to specific groups while the inventory is still being dynamically build in memory based off Terraform output.
NOTE: By default we create a VPC for each droplet on digitalocean. This is to avoid all the droplets ending up in the same default VPC for that region. The template does not support adding multiple droplets in a single custom VPC. The same logic is applied to all other supported cloud providers with their equivalent terminology.
Thus in the ansible code we define a play against the ansible web
group to configure the web host.
---
- hosts: web
vars_file:
- "{{ playbook_dir }}/defaults/main.yml"
- "{{ playbook_dir }}/defaults/template_info.yml"
vars:
ansible_user: "{{ root_username }}"
ansible_ssh_private_key_file: "{{ root_private_key_path }}"
tasks:
- name: Configure our webhosts
ansible.builtin.import_tasks: tasks/web_tasks.yml
tags: always
when:
- "'tfvars' not in ansible_run_tags"
- "'destroy' not in ansible_run_tags"
- "'roles' not in ansible_run_tags"
- "'bootstrap' not in ansible_run_tags"
- "'print_inventory' not in ansible_run_tags"
NOTE: the when
-statement is added to ensure the template tags work 'as expected'. If you do not add the when
-statement the Configure our webhosts
task will run when you supply these tags. A different solution is to replace the always
-tag on the Configure our webhosts
task and supply that tag on each run, for example ansible-playbook --tags=your_custom_tag
.
The example below defines two hosts on digitalocean. We decided that one webserver was not enough, so we added a second one.
host_list: {
digitalocean: [
{ "name": "web01",
"tag": "[\"web\"]"
},
{ "name": "web02",
"tag": "[\"web\"]"
}
]
}
Since we have not defined any other values on both the hosts, the missing values will still use their default settings as show below:
locals {
digitalocean_servers = defaults(var.digitalocean_servers, {
size = "s-1vcpu-1gb"
image = "ubuntu-20-04-x64"
region = "ams3"
backups = false
monitoring = false
ipv6 = false
resize_disk = true
droplet_agent = false
create_vpc = true
})
}
Since both hosts are added to the web group in our inventory we don't need to update our play.
---
- hosts: web
vars_file:
- "{{ playbook_dir }}/defaults/main.yml"
- "{{ playbook_dir }}/defaults/template_info.yml"
vars:
ansible_user: "{{ root_username }}"
ansible_ssh_private_key_file: "{{ root_private_key_path }}"
tasks:
- name: Configure our webhosts
ansible.builtin.import_tasks: tasks/web_tasks.yml
tags: always
when:
- "'tfvars' not in ansible_run_tags"
- "'destroy' not in ansible_run_tags"
- "'roles' not in ansible_run_tags"
- "'bootstrap' not in ansible_run_tags"
- "'print_inventory' not in ansible_run_tags"
The example below defines three hosts on digitalocean. We decided that one our website needed a database, and thus a database server, so we defined a third server in our host_list
. Since we expect our DB to use more resources we changed set its size to s-2vcpu-4g
. We also moved the webhost web01
to a other digitalocean datacenter.
host_list: {
digitalocean: [
{ "name": "web01",
"tag": "[\"web\"]"
"region": "lon1"
},
{ "name": "web02",
"tag": "[\"web\"]"
},
{ "name": "db01",
"tag": "[\"db\"]",
"size": "s-2vcpu-4gb"
}
]
}
All the other values that are missing on each host will again use their default settings as show below:
locals {
digitalocean_servers = defaults(var.digitalocean_servers, {
size = "s-1vcpu-1gb"
image = "ubuntu-20-04-x64"
region = "ams3"
backups = false
monitoring = false
ipv6 = false
resize_disk = true
droplet_agent = false
create_vpc = true
})
}
In the ansible code we would define a additional play against the ansible db
group to configure the database host.
---
- hosts: web
vars_file:
- "{{ playbook_dir }}/defaults/main.yml"
- "{{ playbook_dir }}/defaults/template_info.yml"
vars:
ansible_user: "{{ root_username }}"
ansible_ssh_private_key_file: "{{ root_private_key_path }}"
tasks:
- name: Configure our webhosts
ansible.builtin.import_tasks: tasks/web_tasks.yml
tags: always
when:
- "'tfvars' not in ansible_run_tags"
- "'destroy' not in ansible_run_tags"
- "'roles' not in ansible_run_tags"
- "'bootstrap' not in ansible_run_tags"
- "'print_inventory' not in ansible_run_tags"
- hosts: db
vars_file:
- "{{ playbook_dir }}/defaults/main.yml"
- "{{ playbook_dir }}/defaults/template_info.yml"
vars:
ansible_user: "{{ root_username }}"
ansible_ssh_private_key_file: "{{ root_private_key_path }}"
tasks:
- name: Configure our database hosts
ansible.builtin.import_tasks: tasks/database_tasks.yml
tags: always
when:
- "'tfvars' not in ansible_run_tags"
- "'destroy' not in ansible_run_tags"
- "'roles' not in ansible_run_tags"
- "'bootstrap' not in ansible_run_tags"
- "'print_inventory' not in ansible_run_tags"
The example below defines five hosts split on two different cloud providers. Our previous stack on digitalocean and two additional mail servers on hetzner. Here we also set more options on our VMs, such as hostnames, PTR, regions and images.
host_list: {
digitalocean: [
{ "name": "web01",
"tag": "[\"web\"]"
"hostname": "web01.example.tld",
"region": "lon1",
"image": "debian-11-x64"
},
{ "name": "web02",
"tag": "[\"web\"]",
"hostname": "web02.example.tld",
"region": "ams1",
"image": "debian-11-x64"
},
{ "name": "db01",
"tag": "[\"db\"]",
"hostname": "db01.example.tld",
"size": "s-2vcpu-4gb",
"region": "ams3",
"image": "rancheros"
}
],
hetzner: [
{ "name": "mail01",
"labels": "{mail = \"\"}",
"hostname": "mail01.example.tld",
"ptr": "mail01.example.tld",
"location": "hel1",
"image": "fedora-32"
},
{ "name": "mail02",
"labels": "{mail = \"\"}",
"hostname": "mail02.example.tld",
"ptr": "mail02.example.tld",
"location": "fsn1",
"image": "fedora-32"
}
]
}
NOTE: Digitalocean PTRs are setup automatically ( by digitalocean themselves ) to match the droplet hostname.
All the missing values will again use their default settings as show below:
locals {
digitalocean_servers = defaults(var.digitalocean_servers, {
size = "s-1vcpu-1gb"
image = "ubuntu-20-04-x64"
region = "ams3"
backups = false
monitoring = false
ipv6 = false
resize_disk = true
droplet_agent = false
create_vpc = true
})
}
locals {
hetzner_servers = defaults(var.hetzner_servers, {
server_type = "cx11"
image = "ubuntu-20.04"
location = "nbg1"
backups = false
})
}
In the ansible code we define a additional play against the the ansible mail
group to configure the mail servers.
---
- hosts: web
vars_file:
- "{{ playbook_dir }}/defaults/main.yml"
- "{{ playbook_dir }}/defaults/template_info.yml"
vars:
ansible_user: "{{ root_username }}"
ansible_ssh_private_key_file: "{{ root_private_key_path }}"
tasks:
- name: Configure our webhosts
ansible.builtin.import_tasks: tasks/web_tasks.yml
tags: always
when:
- "'tfvars' not in ansible_run_tags"
- "'destroy' not in ansible_run_tags"
- "'roles' not in ansible_run_tags"
- "'bootstrap' not in ansible_run_tags"
- "'print_inventory' not in ansible_run_tags"
- hosts: db
vars_file:
- "{{ playbook_dir }}/defaults/main.yml"
- "{{ playbook_dir }}/defaults/template_info.yml"
vars:
ansible_user: "{{ root_username }}"
ansible_ssh_private_key_file: "{{ root_private_key_path }}"
tasks:
- name: Configure our database hosts
ansible.builtin.import_tasks: tasks/database_tasks.yml
tags: always
when:
- "'tfvars' not in ansible_run_tags"
- "'destroy' not in ansible_run_tags"
- "'roles' not in ansible_run_tags"
- "'bootstrap' not in ansible_run_tags"
- "'print_inventory' not in ansible_run_tags"
- hosts: mail
vars_file:
- "{{ playbook_dir }}/defaults/main.yml"
- "{{ playbook_dir }}/defaults/template_info.yml"
vars:
ansible_user: "{{ root_username }}"
ansible_ssh_private_key_file: "{{ root_private_key_path }}"
tasks:
- name: Configure our mail hosts
ansible.builtin.import_tasks: tasks/mail_tasks.yml
tags: always
when:
- "'tfvars' not in ansible_run_tags"
- "'destroy' not in ansible_run_tags"
- "'roles' not in ansible_run_tags"
- "'bootstrap' not in ansible_run_tags"
- "'print_inventory' not in ansible_run_tags"
If you want to work on the embeded terraform code itself outside of ansible, you want to setup the terraform.tfvars
-file. You can deploy this file with the following ansible-playbook
command:
ansible-playbook ansible/main.yml --tags=tfvars
Another options is to create a copy of the terraform.tfvars.example
file, fill in the token variables with your API's key and setup the server maps manually.
cp terraform/terraform.tfvars.example terraform/terraform.tfvars
Then cd
to the terraform project folder and run terraform as usual.
terraform init
terraform apply
terraform destroy
NOTE: The ansible playbook creates, overwrites and deletes the terraform.tfvars
file on each run. This is to ensure secrets such as API keys don't linger on disk whenever possible. Don't forget to use ansible-vault if you care about this stuff 😉.
Essentially there is no reason why you couldn't rip out all the current abstracted Terrafrom code from Anster and forgo the host_list logic if you have more requirements than the current Terraform code offers or want to use a cloud provider that currently is not supported yet. Essentially you just need to ensure all the resources you want Anster to do something with are given to Ansible with a Terraform output object. To make your Terraform code work you also want to update the terraform.tfvars.j2 Jinja2 template in ansible/templates/terraform.tfvars.j2
to match your terraform code needs.
Then you would need to ensure you add a matching piece of Ansible code in the ansible/playbooks/terraform/create.yml
file that would add the hosts from your Terraform output to the Ansible inventory. You probably want to to delete all the other Add each deployed $PROVIDER host to ansible inventory
tasks defined in that file and replace it with just your own.
This way Anster would no longer have it's host_list magic, it would however let you use more complex Terraform code and still have all the other nice features such as the dynamic ansible inventory.
You can use Anster in a CI, but you will need to think about a couple of issues. Below are a couple and examples on how to fix these.
- How will you manage your Terraform state file. You can for example using a remote backend at app.terraform.io. Just make sure you set the execution mode of the workspace to
local
. The includedbackend.tf
has an example on how to set up a remote backend at app.terraform.io. - How will you manage your SSH keys. Anster supports adding extra variables on the command line like the following
--extra-vars "{'ssh_key':{'private':'key'},{'public':'key'}}
. When supplied Anster will use these keys instead of auto generating new ones. In order for this to work the private key needs to be in the following format (new lines replaced with\n
)-----BEGIN OPENSSH PRIVATE KEY-----\nb3Blbn[...SNIP...]xyz\n[...SNIP...]QID\n-----END OPENSSH PRIVATE KEY-----\n
. The\n
will be replaced with a new line when the key is saved to disk. Below is an example with Github Actions.
name: CI
on:
workflow_dispatch:
pull_request:
branches:
- main
types: [opened, synchronize, closed]
paths-ignore:
- '.github/**'
- 'README.md'
- 'LICENSE'
- '.gitignore'
- '.editorconfig'
- './ansible/.ansible-lint'
- './ansible/.yamllint'
jobs:
deploy:
if: github.event.pull_request.merged == true # Run only when PR is merged.
runs-on: ubuntu-20.04
name: Run anster
steps:
- name: Check out the codebase.
uses: actions/checkout@v2
- name: Set up Python 3.8.10
uses: actions/setup-python@v2
with:
python-version: '3.8.10'
- name: Install test dependencies.
run: pip3 install ansible yamllint ansible-lint
- uses: hashicorp/setup-terraform@v1
with:
cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}
terraform_wrapper: false
- name: Create secrets.yml
working-directory: ./ansible/defaults
run: cp secrets.example.yml secrets.yml
- name: Run anster create
working-directory: ./ansible
run: ansible-playbook main.yml --tags=create --extra-vars "{'tokens':{'digitalocean':'${{ secrets.DO_API_TOKEN }}'},'ssh_key':{'private':'${{ secrets.SSH_KEY_PRIVATE }}','public':'${{ secrets.SSH_KEY_PUBLIC }}'}}"