This is a follow-up to Using the Morpheus Terraform Provider and Spec Templates, where I was deploying Dynamic DNS (DDNS) using ddns-route53. In the thread we used Spec Templates and the Morpheus Terraform Provider to deploy infrastructure needed to support DDNS.
I mentioned at the end, that there is an Ansible task I created to completely configure the virtual machine that is deployed. Using Morpheus to provide this functionality makes it much easier to link all of the technologies together. Additionally, we’ll be using the Command Bus as part of our integration, which will send the Ansible commands to the VM securely from Morpheus to the agent, without the need of opening any inbound ports. This is effectively giving Ansible an agent, like some of configuration management systems, and also eliminates the need to maintain a service account on each image to authenticate with over the network.
Lets jump into it! Note that Ubuntu 22.04 was used in this deployment.
The Code
In addition to the previous Terraform files, we have the following new file to mention:
- setupAnsible.tf
As well, this is the Ansible playbook that will be used:
- config.yml
Finally, some config file templates that will be copied to the VM and used to setup the service, using Ansible:
- ddns_config.yml
- ddns-route53.service
setupAnsible.tf
:
resource "morpheus_ansible_playbook_task" "ddns_playbook" {
name = "DDNS-Config"
code = "ddns-config"
ansible_repo_id = 7
git_ref = "dev"
playbook = "ddns/ansible/config.yml"
execute_target = "resource"
retryable = true
retry_count = 1
}
resource "morpheus_provisioning_workflow" "ddns_workflow" {
name = "DDNS-Config"
description = "DDNS provisioning workflow"
task {
task_id = morpheus_ansible_playbook_task.ddns_playbook.id
task_phase = "provision"
}
}
Looking at the setupAnsible.tf
above, we are creating two items:
- An Ansible Playbook Task
- A Provisioning Workflow, which will contain the task created in the preceding step
I already have an Ansible integration configured in Morpheus, so I use the static repo ID for the integration here, but you could add the integration as well through the provisioning. We specify the config.yml playbook path to configure the task to run this playbook and the execute target as resource to ensure the playbook is ran against the new target system, not the Morpheus appliance.
We place the task in the Provisioning phase of the workflow, so if it fails the entire deployment will fail. Alternatively, you could add the task to the Post Provisioning phase, which would allow the task to fail and not mark the entire deployment as failed.
Don’t forget to add the setupAnsible.tf
file to the App Blueprint as a Spec Template, like the others mentioned in the previous post!
config.yml
:
---
- name: DDNS Config
gather_facts: false
hosts: all
vars:
accessKeyId: "{{ lookup('cypher','secret=secret/ddns').split('|')[0] }}"
secretAccessKey: "{{ lookup('cypher','secret=secret/ddns').split('|')[1] }}"
tasks:
- name: "Verify that required string variables are defined"
assert:
that:
- "{{ ahs_var }} is defined"
- "{{ ahs_var }} | length > 0"
- "{{ ahs_var }} != None"
fail_msg: "{{ ahs_var }} needs to be set for the role to work"
success_msg: "Required variable {{ ahs_var }} is defined"
loop_control:
loop_var: ahs_var
with_items:
- accessKeyId
- secretAccessKey
- name: Download ddns
ansible.builtin.get_url:
url: https://github.com/crazy-max/ddns-route53/releases/download/v2.8.0/ddns-route53_2.8.0_linux_amd64.tar.gz
dest: /tmp/ddns.tar.gz
mode: '0755'
- name: Create a directory if it does not exist
ansible.builtin.file:
path: /tmp/ddns
state: directory
mode: '0755'
- name: Extract ddns
ansible.builtin.unarchive:
src: /tmp/ddns.tar.gz
dest: /tmp/ddns
remote_src: yes
- name: Ensure group "ddns-route53" exists
ansible.builtin.group:
name: ddns-route53
state: present
- name: Add the user 'ddns-route53' and a group of 'ddns-route53'
ansible.builtin.user:
name: ddns-route53
comment: Dynamic DNS
group: ddns-route53
shell: /bin/false
home: /bin/null
- name: Create a directory if it does not exist
ansible.builtin.file:
path: /etc/ddns-route53/
state: directory
mode: '0755'
- name: Create ddns config
ansible.builtin.template:
src: ./templates/ddns_config.yml
dest: /etc/ddns-route53/ddns-route53.yml
owner: ddns-route53
group: ddns-route53
mode: '0644'
- name: Copy file with owner and permissions
ansible.builtin.copy:
src: /tmp/ddns/ddns-route53
dest: /usr/local/bin/ddns-route53
owner: ddns-route53
group: ddns-route53
mode: '0744'
remote_src: yes
- name: Template a file to /etc/ddns-route53/ddns-route53.yml
ansible.builtin.template:
src: ./templates/ddns-route53.service
dest: /etc/systemd/system/ddns-route53.service
owner: ddns-route53
group: ddns-route53
mode: '0644'
- name: Start service ddns-route53, if not started, and enable
ansible.builtin.service:
name: ddns-route53
state: started
enabled: yes
There are a few items to note in the Ansible playbook config.yml
above. We are looking up Cypher values, which will be used in the playbook. Using Cypher in Ansible is a different format than when we used them in Terraform and other task types, which more info can be found here.
Many of these steps are basically following the documentation for ddns-route53 documentation, at a high level we are:
- Downloading and unpacking the software
- Create the user/group needed
- Copying the configuration file template (
ddns_config.yml
) for the service to be able to connect AWS - Copying the configuration file (
ddns-route53.service
) for the service to run automatically
ddns_config.yml
and ddns-route53.service
are shown below for visibility.
ddns_config.yml
(some values modified, which could be dynamic):
credentials:
accessKeyID: "{{ accessKeyId }}"
secretAccessKey: "{{ secretAccessKey }}"
route53:
hostedZoneID: "AAAAAAABBBBBBBCCCCCCC"
recordsSet:
- name: "morpheus.example.com."
type: "A"
ttl: 300
ddns-route53.service
:
[Unit]
Description=ddns-route53
Documentation=https://crazymax.dev/ddns-route53/
After=syslog.target
After=network.target
[Service]
RestartSec=2s
Type=simple
User=ddns-route53
Group=ddns-route53
ExecStart=/usr/local/bin/ddns-route53 --config /etc/ddns-route53/ddns-route53.yml
Restart=always
Environment=SCHEDULE="*/30 * * * *"
[Install]
WantedBy=multi-user.target
That’s it! Once deployed, all of your infrastructure is built (mostly from the last topic) and the additional tasks/workflows to support the configuration of the DDNS service. You would see something similar to this in the history of the instance deployed, confirming the VM has been configure to spec!
The service will begin to update your hosted zone record in Route 53 with your public IP address.