{"id":3637,"date":"2025-12-28T23:59:53","date_gmt":"2025-12-28T20:59:53","guid":{"rendered":"https:\/\/www.dchost.com\/blog\/automating-vps-setup-with-terraform-and-ansible\/"},"modified":"2025-12-28T23:59:53","modified_gmt":"2025-12-28T20:59:53","slug":"automating-vps-setup-with-terraform-and-ansible","status":"publish","type":"post","link":"https:\/\/www.dchost.com\/blog\/en\/automating-vps-setup-with-terraform-and-ansible\/","title":{"rendered":"Automating VPS Setup with Terraform and Ansible"},"content":{"rendered":"<div class=\"dchost-blog-content-wrapper\"><p>If you find yourself repeating the same <a href=\"https:\/\/www.dchost.com\/vps\">VPS<\/a> setup steps again and again\u2014creating users, hardening SSH, installing Docker or a web stack, opening the same ports\u2014it is time to treat your servers like code. With Terraform and Ansible, you can turn the entire VPS lifecycle into a predictable pipeline: provision the machine, configure the OS and services, and be ready for production at the push of a button. In this article, we will walk through how we approach reproducible VPS builds as the dchost.com team, from designing the folder layout to wiring Terraform outputs into Ansible inventories. The goal is simple: every new VPS, whether it lives on a dchost VPS plan, a <a href=\"https:\/\/www.dchost.com\/dedicated-server\">dedicated server<\/a> or a colocation box you host with us, should follow the same automated recipe. That means faster launches, fewer mistakes, and an infrastructure you can rebuild on demand if you ever need to scale out or recover from a disaster.<\/p>\n<div id=\"toc_container\" class=\"toc_transparent no_bullets\"><p class=\"toc_title\">\u0130&ccedil;indekiler<\/p><ul class=\"toc_list\"><li><a href=\"#Why_Reproducible_VPS_Builds_Matter\"><span class=\"toc_number toc_depth_1\">1<\/span> Why Reproducible VPS Builds Matter<\/a><\/li><li><a href=\"#Terraform_and_Ansible_Who_Does_What\"><span class=\"toc_number toc_depth_1\">2<\/span> Terraform and Ansible: Who Does What?<\/a><ul><li><a href=\"#Terraform_Infrastructure_as_Code\"><span class=\"toc_number toc_depth_2\">2.1<\/span> Terraform: Infrastructure as Code<\/a><\/li><li><a href=\"#Ansible_Configuration_Management_and_Orchestration\"><span class=\"toc_number toc_depth_2\">2.2<\/span> Ansible: Configuration Management and Orchestration<\/a><\/li><li><a href=\"#The_Simple_Rule_of_Thumb\"><span class=\"toc_number toc_depth_2\">2.3<\/span> The Simple Rule of Thumb<\/a><\/li><\/ul><\/li><li><a href=\"#Designing_Your_Automated_VPS_Stack_on_dchost\"><span class=\"toc_number toc_depth_1\">3<\/span> Designing Your Automated VPS Stack on dchost<\/a><ul><li><a href=\"#Prerequisites\"><span class=\"toc_number toc_depth_2\">3.1<\/span> Prerequisites<\/a><\/li><\/ul><\/li><li><a href=\"#Step_1_Defining_VPS_Infrastructure_with_Terraform\"><span class=\"toc_number toc_depth_1\">4<\/span> Step 1 \u2013 Defining VPS Infrastructure with Terraform<\/a><ul><li><a href=\"#Basic_Terraform_Configuration\"><span class=\"toc_number toc_depth_2\">4.1<\/span> Basic Terraform Configuration<\/a><\/li><li><a href=\"#Terraform_Workflow\"><span class=\"toc_number toc_depth_2\">4.2<\/span> Terraform Workflow<\/a><\/li><\/ul><\/li><li><a href=\"#Step_2_Provisioning_the_Server_with_Ansible\"><span class=\"toc_number toc_depth_1\">5<\/span> Step 2 \u2013 Provisioning the Server with Ansible<\/a><ul><li><a href=\"#Building_the_Inventory_from_Terraform_Outputs\"><span class=\"toc_number toc_depth_2\">5.1<\/span> Building the Inventory from Terraform Outputs<\/a><\/li><li><a href=\"#Writing_a_Base_Hardening_Role\"><span class=\"toc_number toc_depth_2\">5.2<\/span> Writing a Base Hardening Role<\/a><\/li><li><a href=\"#Installing_a_Web_Stack_Role\"><span class=\"toc_number toc_depth_2\">5.3<\/span> Installing a Web Stack Role<\/a><\/li><li><a href=\"#Bringing_Roles_Together_in_a_Playbook\"><span class=\"toc_number toc_depth_2\">5.4<\/span> Bringing Roles Together in a Playbook<\/a><\/li><\/ul><\/li><li><a href=\"#Step_3_Orchestrating_Terraform_and_Ansible_Together\"><span class=\"toc_number toc_depth_1\">6<\/span> Step 3 \u2013 Orchestrating Terraform and Ansible Together<\/a><ul><li><a href=\"#Using_a_Makefile_as_a_Simple_Orchestrator\"><span class=\"toc_number toc_depth_2\">6.1<\/span> Using a Makefile as a Simple Orchestrator<\/a><\/li><li><a href=\"#Integrating_with_CICD\"><span class=\"toc_number toc_depth_2\">6.2<\/span> Integrating with CI\/CD<\/a><\/li><\/ul><\/li><li><a href=\"#Adding_DNS_SSL_Monitoring_and_Backups\"><span class=\"toc_number toc_depth_1\">7<\/span> Adding DNS, SSL, Monitoring and Backups<\/a><ul><li><a href=\"#Managing_DNS_with_Terraform\"><span class=\"toc_number toc_depth_2\">7.1<\/span> Managing DNS with Terraform<\/a><\/li><li><a href=\"#Automating_SSL_certificates\"><span class=\"toc_number toc_depth_2\">7.2<\/span> Automating SSL certificates<\/a><\/li><li><a href=\"#Monitoring_and_Alerts\"><span class=\"toc_number toc_depth_2\">7.3<\/span> Monitoring and Alerts<\/a><\/li><li><a href=\"#Backups_and_Off-Site_Storage\"><span class=\"toc_number toc_depth_2\">7.4<\/span> Backups and Off-Site Storage<\/a><\/li><\/ul><\/li><li><a href=\"#Common_Pitfalls_and_How_to_Avoid_Them\"><span class=\"toc_number toc_depth_1\">8<\/span> Common Pitfalls and How to Avoid Them<\/a><ul><li><a href=\"#Forgetting_About_SSH_Connectivity\"><span class=\"toc_number toc_depth_2\">8.1<\/span> Forgetting About SSH Connectivity<\/a><\/li><li><a href=\"#Non-Idempotent_Ansible_Tasks\"><span class=\"toc_number toc_depth_2\">8.2<\/span> Non-Idempotent Ansible Tasks<\/a><\/li><li><a href=\"#Mixing_Manual_Changes_with_Automation\"><span class=\"toc_number toc_depth_2\">8.3<\/span> Mixing Manual Changes with Automation<\/a><\/li><li><a href=\"#Secrets_Management\"><span class=\"toc_number toc_depth_2\">8.4<\/span> Secrets Management<\/a><\/li><\/ul><\/li><li><a href=\"#Where_This_Fits_in_Your_Hosting_Strategy_with_dchost\"><span class=\"toc_number toc_depth_1\">9<\/span> Where This Fits in Your Hosting Strategy with dchost<\/a><\/li><li><a href=\"#Bringing_It_All_Together\"><span class=\"toc_number toc_depth_1\">10<\/span> Bringing It All Together<\/a><\/li><\/ul><\/div>\n<h2><span id=\"Why_Reproducible_VPS_Builds_Matter\">Why Reproducible VPS Builds Matter<\/span><\/h2>\n<p>Before diving into tools, it is worth clarifying why this level of automation is worth the effort. A manually built VPS typically follows a checklist in someone\u2019s notebook or wiki page. Over time, people skip steps, tweak configurations directly on the server, or forget to apply improvements everywhere. This leads to configuration drift: two servers that are supposed to be identical behave differently, and debugging becomes guesswork.<\/p>\n<p>Reproducible VPS builds change that. You declare in code exactly how a machine should look\u2014CPU\/RAM, disk, OS image, installed packages, users, SSH keys, firewall rules, monitoring agents, backup scripts. Then you let tools like Terraform and Ansible apply that declaration in a repeatable way. If you ever lose a VPS or need a second one, you simply re-run the pipeline. Combined with a solid backup strategy, this is also a massive boost for disaster recovery; if you want to go deeper on that side, we recommend our guide on <a href=\"https:\/\/www.dchost.com\/blog\/en\/felaket-kurtarma-plani-nasil-yazilir-rto-rpoyu-kafada-netlestirip-yedek-testleri-ve-runbooklari-gercekten-calisir-hale-getirmek\/\">writing a realistic disaster recovery plan with tested runbooks<\/a>.<\/p>\n<p>We also see this approach pay off during security work. Instead of hardening each VPS by hand, you encode your best practices once and roll them out everywhere. Our detailed checklist for <a href=\"https:\/\/www.dchost.com\/blog\/en\/vps-guvenlik-sertlestirme-kontrol-listesi-sshd_config-fail2ban-ve-root-erisimini-kapatmak\/\">VPS security hardening (sshd_config, Fail2ban, no-root SSH)<\/a> fits perfectly into Ansible roles that run on every new server automatically.<\/p>\n<h2><span id=\"Terraform_and_Ansible_Who_Does_What\">Terraform and Ansible: Who Does What?<\/span><\/h2>\n<p>Terraform and Ansible overlap conceptually, but in practice they shine at different layers of the stack. Understanding that division of responsibilities will keep your automation simple and maintainable.<\/p>\n<h3><span id=\"Terraform_Infrastructure_as_Code\">Terraform: Infrastructure as Code<\/span><\/h3>\n<p><strong>Terraform<\/strong> is an infrastructure-as-code (IaC) tool. You describe the infrastructure you want\u2014VPS instances, disks, IP addresses, networks, DNS records\u2014in declarative configuration files. Terraform compares your desired state with the actual state and then creates, updates or destroys resources to match.<\/p>\n<p>For VPS automation, you typically let Terraform handle:<\/p>\n<ul>\n<li>Creating the VPS itself (vCPU\/RAM\/disk plan, OS image)<\/li>\n<li>Assigning public IPs and networking options<\/li>\n<li>Managing DNS records for hostnames<\/li>\n<li>Outputting connection details (IP, SSH port, usernames) for later steps<\/li>\n<\/ul>\n<p>If you want a deeper Terraform-focused perspective, we have a dedicated article on <a href=\"https:\/\/www.dchost.com\/blog\/en\/terraform-ile-vps-ve-dns-otomasyonu-cloudflare-proxmox-openstack-ve-sifir-kesinti-dagitim-nasil-bir-araya-gelir\/\">automating VPS and DNS with Terraform and achieving zero-downtime deploys<\/a>.<\/p>\n<h3><span id=\"Ansible_Configuration_Management_and_Orchestration\">Ansible: Configuration Management and Orchestration<\/span><\/h3>\n<p><strong>Ansible<\/strong> focuses on what happens inside the server. You describe the desired state of packages, services, configuration files, users and permissions. Ansible connects via SSH and makes idempotent changes\u2014if the system already matches the desired state, it does nothing; if not, it fixes it.<\/p>\n<p>For VPS automation, Ansible is ideal for:<\/p>\n<ul>\n<li>Creating non-root users and authorized SSH keys<\/li>\n<li>Locking down SSH, installing Fail2ban, configuring firewalls<\/li>\n<li>Installing web stacks (Nginx, PHP-FPM, Node.js, Docker, databases)<\/li>\n<li>Deploying application code and setting up systemd services<\/li>\n<li>Configuring backups, monitoring agents and log shippers<\/li>\n<\/ul>\n<p>We use Ansible heavily together with cloud-init; if you want another angle on that, see our practical story on <a href=\"https:\/\/www.dchost.com\/blog\/en\/bulutun-ilk-nefesi-cloud%e2%80%91init-ve-ansible-ile-tekrar-uretilebilir-vps-nasil-kurulur\/\">turning a blank VPS into a ready-to-serve machine with cloud-init and Ansible<\/a>.<\/p>\n<h3><span id=\"The_Simple_Rule_of_Thumb\">The Simple Rule of Thumb<\/span><\/h3>\n<p>A helpful way to remember the split:<\/p>\n<ul>\n<li><strong>Terraform<\/strong>: \u201cGive me three VPS machines with these specs and hostnames.\u201d<\/li>\n<li><strong>Ansible<\/strong>: \u201cOn each VPS, create these users, install this stack and configure these services.\u201d<\/li>\n<\/ul>\n<p>Once that is clear, wiring them together becomes much easier.<\/p>\n<h2><span id=\"Designing_Your_Automated_VPS_Stack_on_dchost\">Designing Your Automated VPS Stack on dchost<\/span><\/h2>\n<p>At dchost.com we like to start with a simple repository layout that you can grow over time. You can host this in Git and connect it to CI\/CD later.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">infra-project\/\n  terraform\/\n    main.tf\n    variables.tf\n    outputs.tf\n    provider.tf\n    environments\/\n      staging\/\n        terraform.tfvars\n      production\/\n        terraform.tfvars\n  ansible\/\n    inventory\/\n      hosts.ini\n    group_vars\/\n      all.yml\n    roles\/\n      base\/\n      webserver\/\n      monitoring\/\n      backup\/\n    playbooks\/\n      site.yml\n      hardening.yml\n<\/code><\/pre>\n<p>This separation keeps responsibilities clear: Terraform defines and creates VPS instances under <code>terraform\/<\/code>, while Ansible configures them under <code>ansible\/<\/code>.<\/p>\n<h3><span id=\"Prerequisites\">Prerequisites<\/span><\/h3>\n<p>To follow a similar setup on dchost infrastructure, you will want:<\/p>\n<ul>\n<li>A dchost VPS, dedicated server or colocation server where you can reach the hypervisor or API (depending on your architecture).<\/li>\n<li>Terraform installed on your local machine or CI server.<\/li>\n<li>Ansible installed locally or in a build container.<\/li>\n<li>At least one SSH key pair ready (we usually store public keys in Ansible vars and upload them automatically).<\/li>\n<li>A Git repository to version-control your Terraform and Ansible code.<\/li>\n<\/ul>\n<p>If you are new to VPS administration itself, it is worth reading our guide on <a href=\"https:\/\/www.dchost.com\/blog\/en\/yeni-vpste-ilk-24-saat-guncelleme-guvenlik-duvari-ve-kullanici-hesaplari\/\">what to do in the first 24 hours on a new VPS<\/a>. Much of that checklist is exactly what we will automate with Ansible in this article.<\/p>\n<h2><span id=\"Step_1_Defining_VPS_Infrastructure_with_Terraform\">Step 1 \u2013 Defining VPS Infrastructure with Terraform<\/span><\/h2>\n<p>We will keep the Terraform examples provider-agnostic, because the exact resource names depend on which API or integration you use to control your VPS at dchost. Conceptually, however, every provider-specific module will have the same ingredients: plan, disk, image, network and SSH key.<\/p>\n<h3><span id=\"Basic_Terraform_Configuration\">Basic Terraform Configuration<\/span><\/h3>\n<p>Start by defining variables for things that will change per environment: hostname prefix, number of instances, plan size, region and SSH key.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">\/\/ terraform\/variables.tf\nvariable &quot;project&quot; {\n  type        = string\n  description = &quot;Project name prefix for resources&quot;\n}\n\nvariable &quot;environment&quot; {\n  type        = string\n  description = &quot;Environment name (staging, production, etc.)&quot;\n}\n\nvariable &quot;vps_count&quot; {\n  type        = number\n  description = &quot;How many VPS instances to create&quot;\n  default     = 1\n}\n\nvariable &quot;ssh_public_key&quot; {\n  type        = string\n  description = &quot;SSH public key to install on VPS instances&quot;\n}\n<\/code><\/pre>\n<p>Then define a simple VPS resource. Here we use a fictional <code>myvps_server<\/code> resource to keep things generic; you would replace this with the real resource type matching how you orchestrate dchost infrastructure.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">\/\/ terraform\/main.tf\nresource &quot;myvps_server&quot; &quot;app&quot; {\n  count    = var.vps_count\n  name     = &quot;${var.project}-${var.environment}-${count.index + 1}&quot;\n  plan     = &quot;nvme-2vcpu-4gb&quot;    \/\/ example plan name\n  region   = &quot;eu-central&quot;        \/\/ or your preferred data center\n  image    = &quot;ubuntu-22-04&quot;      \/\/ or Debian\/AlmaLinux etc.\n\n  ssh_keys = [var.ssh_public_key]\n\n  \/\/ Optional: cloud-init to bootstrap before Ansible\n  user_data = file(&quot;cloud-init.yml&quot;)\n}\n<\/code><\/pre>\n<p>Finally, expose the outputs that Ansible will use later: public IPs and hostnames.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">\/\/ terraform\/outputs.tf\noutput &quot;app_ips&quot; {\n  description = &quot;Public IPs of the app servers&quot;\n  value       = [for s in myvps_server.app : s.ipv4_address]\n}\n\noutput &quot;app_hostnames&quot; {\n  description = &quot;Hostnames of the app servers&quot;\n  value       = [for s in myvps_server.app : s.name]\n}\n<\/code><\/pre>\n<h3><span id=\"Terraform_Workflow\">Terraform Workflow<\/span><\/h3>\n<p>With the configuration in place, your workflow looks like this:<\/p>\n<ol>\n<li><strong>Initialize<\/strong> Terraform once:\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">cd terraform\nterraform init\n<\/code><\/pre>\n<\/li>\n<li><strong>Set environment-specific variables<\/strong> in <code>environments\/staging\/terraform.tfvars<\/code> etc:<\/li>\n<\/ol>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">\/\/ terraform\/environments\/staging\/terraform.tfvars\nproject        = &quot;shop&quot;\nenvironment    = &quot;staging&quot;\nvps_count      = 2\nssh_public_key = &quot;ssh-ed25519 AAAA... your-key-here&quot;\n<\/code><\/pre>\n<ol start=\"3\">\n<li><strong>Plan<\/strong> the changes:<\/li>\n<\/ol>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">terraform plan -var-file=&quot;environments\/staging\/terraform.tfvars&quot;\n<\/code><\/pre>\n<ol start=\"4\">\n<li><strong>Apply<\/strong> to actually create the VPS instances:<\/li>\n<\/ol>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">terraform apply -var-file=&quot;environments\/staging\/terraform.tfvars&quot;\n<\/code><\/pre>\n<p>Terraform will output the IPs and hostnames you defined in <code>outputs.tf<\/code>. We will consume those from Ansible in the next step.<\/p>\n<h2><span id=\"Step_2_Provisioning_the_Server_with_Ansible\">Step 2 \u2013 Provisioning the Server with Ansible<\/span><\/h2>\n<p>Once Terraform has created the VPS, Ansible takes over to turn a plain OS into a production-ready server. You can run Ansible from your laptop, a CI job, or a management VM inside your dchost environment.<\/p>\n<h3><span id=\"Building_the_Inventory_from_Terraform_Outputs\">Building the Inventory from Terraform Outputs<\/span><\/h3>\n<p>Ansible needs to know which hosts to connect to. The simplest approach is to generate a static inventory file after Terraform runs. For small setups, you can paste IPs manually; for larger ones, you can use a script or Terraform\u2019s <code>local_file<\/code> resource to render <code>hosts.ini<\/code> automatically.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># ansible\/inventory\/hosts.ini\n[app]\n203.0.113.10\n203.0.113.11\n\n[app:vars]\nansible_user=ubuntu\nansible_ssh_private_key_file=~\/.ssh\/id_ed25519\n<\/code><\/pre>\n<p>Now Ansible knows it has an <code>app<\/code> group with two hosts and how to connect to them.<\/p>\n<h3><span id=\"Writing_a_Base_Hardening_Role\">Writing a Base Hardening Role<\/span><\/h3>\n<p>We strongly recommend putting security basics into a reusable role so every new VPS starts hardened by default. Here is a simplified example that touches on some of the points from our <a href=\"https:\/\/www.dchost.com\/blog\/en\/vps-guvenlik-sertlestirme-kontrol-listesi-sshd_config-fail2ban-ve-root-erisimini-kapatmak\/\">VPS security hardening checklist<\/a>.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># ansible\/roles\/base\/tasks\/main.yml\n---\n- name: Ensure apt cache is up to date\n  apt:\n    update_cache: yes\n    cache_valid_time: 3600\n\n- name: Upgrade all packages (safe)\n  apt:\n    upgrade: safe\n  when: ansible_os_family == &quot;Debian&quot;\n\n- name: Create non-root deploy user\n  user:\n    name: deploy\n    shell: \/bin\/bash\n    groups: sudo\n    append: yes\n\n- name: Authorize SSH key for deploy user\n  authorized_key:\n    user: deploy\n    key: &quot;{{ lookup('file', '~\/.ssh\/id_ed25519.pub') }}&quot;\n\n- name: Disable root SSH login and password auth\n  lineinfile:\n    path: \/etc\/ssh\/sshd_config\n    regexp: '^{{ item.key }}'\n    line: '{{ item.key }} {{ item.value }}'\n    state: present\n  loop:\n    - { key: 'PermitRootLogin', value: 'no' }\n    - { key: 'PasswordAuthentication', value: 'no' }\n  notify: Restart sshd\n\n- name: Ensure uncomplicated firewall (ufw) is installed\n  apt:\n    name: ufw\n    state: present\n\n- name: Allow SSH and HTTP\/HTTPS through ufw\n  ufw:\n    rule: allow\n    name: &quot;{{ item }}&quot;\n  loop:\n    - OpenSSH\n    - 'Nginx Full'\n\n- name: Enable ufw\n  ufw:\n    state: enabled\n    policy: deny\n\n<\/code><\/pre>\n<p>Handlers restart services when configuration files change:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># ansible\/roles\/base\/handlers\/main.yml\n---\n- name: Restart sshd\n  service:\n    name: ssh\n    state: restarted\n<\/code><\/pre>\n<h3><span id=\"Installing_a_Web_Stack_Role\">Installing a Web Stack Role<\/span><\/h3>\n<p>Next, create a simple webserver role to install Nginx and PHP-FPM (or another stack that matches your application). This is also where you can apply PHP-FPM tuning similar to what we explain in our guide on <a href=\"https:\/\/www.dchost.com\/blog\/en\/wordpress-ve-woocommerce-icin-php-fpm-ayarlari-pm-pm-max_children-ve-pm-max_requests-hesaplama-rehberi\/\">PHP-FPM settings for high-performance WordPress and WooCommerce<\/a>.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># ansible\/roles\/webserver\/tasks\/main.yml\n---\n- name: Install Nginx and PHP-FPM\n  apt:\n    name:\n      - nginx\n      - php-fpm\n    state: present\n\n- name: Ensure Nginx is enabled and running\n  service:\n    name: nginx\n    state: started\n    enabled: yes\n\n- name: Ensure PHP-FPM is enabled and running\n  service:\n    name: php-fpm\n    state: started\n    enabled: yes\n\n- name: Deploy Nginx vhost for app\n  template:\n    src: vhost.conf.j2\n    dest: \/etc\/nginx\/sites-available\/app.conf\n  notify: Reload nginx\n\n- name: Enable app vhost\n  file:\n    src: \/etc\/nginx\/sites-available\/app.conf\n    dest: \/etc\/nginx\/sites-enabled\/app.conf\n    state: link\n  notify: Reload nginx\n<\/code><\/pre>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># ansible\/roles\/webserver\/handlers\/main.yml\n---\n- name: Reload nginx\n  service:\n    name: nginx\n    state: reloaded\n<\/code><\/pre>\n<h3><span id=\"Bringing_Roles_Together_in_a_Playbook\">Bringing Roles Together in a Playbook<\/span><\/h3>\n<p>Now create a main playbook that applies both roles to the <code>app<\/code> group:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># ansible\/playbooks\/site.yml\n---\n- hosts: app\n  become: yes\n  roles:\n    - base\n    - webserver\n<\/code><\/pre>\n<p>Run it like this:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">cd ansible\nansible-playbook -i inventory\/hosts.ini playbooks\/site.yml\n<\/code><\/pre>\n<p>After a few minutes, your Terraform-created VPS should be locked down, updated and serving a basic web stack.<\/p>\n<h2><span id=\"Step_3_Orchestrating_Terraform_and_Ansible_Together\">Step 3 \u2013 Orchestrating Terraform and Ansible Together<\/span><\/h2>\n<p>So far we have run Terraform and Ansible as separate commands. To feel like \u201cone button\u201d, we need a tiny bit of orchestration. You do not need a full-blown platform for this; a Makefile or a shell script is often enough.<\/p>\n<h3><span id=\"Using_a_Makefile_as_a_Simple_Orchestrator\">Using a Makefile as a Simple Orchestrator<\/span><\/h3>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># Makefile\nENV ?= staging\n\nTF_DIR = terraform\nANSIBLE_DIR = ansible\nTF_VARS = $(TF_DIR)\/environments\/$(ENV)\/terraform.tfvars\n\n.PHONY: plan apply destroy provision all\n\nplan:\n\tcd $(TF_DIR) &amp;&amp; terraform plan -var-file=$(TF_VARS)\n\napply:\n\tcd $(TF_DIR) &amp;&amp; terraform apply -auto-approve -var-file=$(TF_VARS)\n\nprovision:\n\tcd $(ANSIBLE_DIR) &amp;&amp; ansible-playbook -i inventory\/hosts.ini playbooks\/site.yml\n\nall: apply provision\n<\/code><\/pre>\n<p>Now the entire pipeline becomes:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">make ENV=staging all\n<\/code><\/pre>\n<p>Behind the scenes, Terraform creates or updates the VPS, and Ansible configures it. When your team gets used to this workflow, spinning up extra capacity or a staging environment becomes a routine command instead of a mini-project.<\/p>\n<h3><span id=\"Integrating_with_CICD\">Integrating with CI\/CD<\/span><\/h3>\n<p>Later, you can move this into CI\/CD. For example:<\/p>\n<ul>\n<li>On pushes to <code>main<\/code>, your pipeline runs <code>terraform plan<\/code> for review.<\/li>\n<li>On approved merges or tagged releases, it runs <code>terraform apply<\/code> followed by <code>ansible-playbook<\/code>.<\/li>\n<li>Environment selection (staging\/production) is driven by branch or tag naming conventions.<\/li>\n<\/ul>\n<p>This keeps the entire lifecycle auditable in Git: who changed what, when and why.<\/p>\n<h2><span id=\"Adding_DNS_SSL_Monitoring_and_Backups\">Adding DNS, SSL, Monitoring and Backups<\/span><\/h2>\n<p>A VPS that serves traffic in production needs more than just Nginx. With Terraform and Ansible in place, it is straightforward to extend your automation to DNS, TLS, monitoring and backups.<\/p>\n<h3><span id=\"Managing_DNS_with_Terraform\">Managing DNS with Terraform<\/span><\/h3>\n<p>Most DNS providers have Terraform support, and you can also automate DNS for domains you host through dchost.com by wiring Terraform to the relevant APIs or templates. The idea is to let Terraform create A\/AAAA records for each VPS, so hostnames always match the infrastructure state.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">resource &quot;mydns_record&quot; &quot;app&quot; {\n  count   = length(myvps_server.app)\n  zone    = &quot;example.com&quot;\n  name    = &quot;app-${count.index + 1}&quot;\n  type    = &quot;A&quot;\n  value   = myvps_server.app[count.index].ipv4_address\n  ttl     = 300\n}\n<\/code><\/pre>\n<p>We go deeper into this style of automation in our article on <a href=\"https:\/\/www.dchost.com\/blog\/en\/terraform-ile-vps-ve-dns-otomasyonu-cloudflare-proxmox-openstack-ve-sifir-kesinti-dagitim-nasil-bir-araya-gelir\/\">Terraform-based VPS and DNS automation for zero-downtime deployments<\/a>.<\/p>\n<h3><span id=\"Automating_SSL_certificates\">Automating <a href=\"https:\/\/www.dchost.com\/ssl\">SSL certificate<\/a>s<\/span><\/h3>\n<p>Once DNS is in place, you want HTTPS. A common pattern is:<\/p>\n<ul>\n<li>Ansible installs a web server and ACME client (such as certbot or acme.sh).<\/li>\n<li>Playbooks request and renew certificates automatically.<\/li>\n<li>Nginx\/Apache templates reference the certificate paths.<\/li>\n<\/ul>\n<p>This can be either HTTP-01 or DNS-01 based, depending on your DNS setup and whether you need wildcards. For a practical deep dive, including multi-domain and wildcard strategies, see our guide on <a href=\"https:\/\/www.dchost.com\/blog\/en\/lets-encrypt-wildcard-ssl-otomasyonu-dns-01-ile-cpanel-plesk-ve-nginxte-zahmetsiz-kurulum-ve-yenileme-nasil-yapilir\/\">Let\u2019s Encrypt wildcard SSL automation<\/a>.<\/p>\n<h3><span id=\"Monitoring_and_Alerts\">Monitoring and Alerts<\/span><\/h3>\n<p>Automation makes it easy to add monitoring agents to every new VPS. A common pattern on our side is:<\/p>\n<ul>\n<li>Terraform labels instances with environment and role.<\/li>\n<li>Ansible installs and configures exporters or agents (Node Exporter, Promtail, etc.).<\/li>\n<li>A central Prometheus + Grafana stack scrapes or receives metrics and logs.<\/li>\n<\/ul>\n<p>If you want to set up a basic monitoring stack quickly, we have a beginner-friendly walkthrough on <a href=\"https:\/\/www.dchost.com\/blog\/en\/vps-izleme-ve-alarm-kurulumu-prometheus-grafana-ve-uptime-kuma-ile-baslangic\/\">VPS monitoring and alerts with Prometheus, Grafana and Uptime Kuma<\/a>. The same Ansible roles you use there can be attached to any new VPS created by Terraform.<\/p>\n<h3><span id=\"Backups_and_Off-Site_Storage\">Backups and Off-Site Storage<\/span><\/h3>\n<p>Backups are the final pillar of a production-ready VPS. The nice thing about infrastructure as code is that you can fully encode your backup strategy as well:<\/p>\n<ul>\n<li>Ansible installs tools like restic or Borg.<\/li>\n<li>Playbooks configure backup targets (S3-compatible object storage, NFS, etc.), credentials and schedules.<\/li>\n<li>Systemd timers or cron jobs run backups on a regular cadence.<\/li>\n<\/ul>\n<p>We have a dedicated guide on <a href=\"https:\/\/www.dchost.com\/blog\/en\/restic-ve-borg-ile-s3-uyumlu-uzak-yedekleme-surumleme-sifreleme-ve-saklama-ne-zaman-nasil\/\">offsite backups with restic\/Borg and S3-compatible storage (versioning, encryption and retention)<\/a> that slots neatly into Ansible roles.<\/p>\n<h2><span id=\"Common_Pitfalls_and_How_to_Avoid_Them\">Common Pitfalls and How to Avoid Them<\/span><\/h2>\n<p>Terraform + Ansible workflows are powerful, but there are a few gotchas that we see repeatedly in real projects. The good news: most of them are easy to avoid once you know what to look for.<\/p>\n<h3><span id=\"Forgetting_About_SSH_Connectivity\">Forgetting About SSH Connectivity<\/span><\/h3>\n<p>Terraform might successfully create a VPS that Ansible cannot reach. Common reasons:<\/p>\n<ul>\n<li>Firewall or security group does not allow SSH from your Ansible runner.<\/li>\n<li>The default username (e.g. <code>ubuntu<\/code>, <code>debian<\/code>, <code>root<\/code>) is different from what you assumed.<\/li>\n<li>Your SSH key was not injected correctly (cloud-init misconfiguration).<\/li>\n<\/ul>\n<p>To avoid this, test SSH manually as soon as Terraform finishes, and standardize your base image or cloud-init so it always has the same initial user and SSH configuration.<\/p>\n<h3><span id=\"Non-Idempotent_Ansible_Tasks\">Non-Idempotent Ansible Tasks<\/span><\/h3>\n<p>Ansible\u2019s power lies in idempotence: running the same playbook multiple times should not break anything. Pitfalls include:<\/p>\n<ul>\n<li>Using shell commands that <em>append<\/em> to files on every run.<\/li>\n<li>Downloading archives into the same directory without cleanup.<\/li>\n<li>Manipulating configuration files with fragile <code>sed<\/code> or <code>lineinfile<\/code> rules.<\/li>\n<\/ul>\n<p>Favour Ansible modules (like <code>apt<\/code>, <code>user<\/code>, <code>template<\/code>, <code>ufw<\/code>) over raw shell commands, and test your playbooks by running them twice on a fresh VPS to see if the second run reports \u201cok\u201d instead of \u201cchanged\u201d.<\/p>\n<h3><span id=\"Mixing_Manual_Changes_with_Automation\">Mixing Manual Changes with Automation<\/span><\/h3>\n<p>It is tempting to \u201cjust tweak one thing\u201d directly on a live server. Over time these manual edits diverge from what your Ansible roles expect, and future runs might undo your fixes or fail in surprising ways.<\/p>\n<p>A healthier approach is to treat Terraform and Ansible as the single source of truth. When you need a change, commit it to Git, run the pipeline and let the automation apply it. This is especially important for security-related changes and firewall rules.<\/p>\n<h3><span id=\"Secrets_Management\">Secrets Management<\/span><\/h3>\n<p>Never hard-code passwords, API keys or database credentials in plain-text Ansible vars or Terraform files. Use at least:<\/p>\n<ul>\n<li>Ansible Vault to encrypt sensitive variables.<\/li>\n<li>Environment variables or secret storage in your CI system.<\/li>\n<li>Dedicated secret management tools if your stack grows (e.g. HashiCorp Vault, SOPS + age, etc.).<\/li>\n<\/ul>\n<p>We follow similar patterns in our own infrastructure when automating VPS deployments and see a huge reduction in \u201cleaked secrets in Git\u201d incidents.<\/p>\n<h2><span id=\"Where_This_Fits_in_Your_Hosting_Strategy_with_dchost\">Where This Fits in Your Hosting Strategy with dchost<\/span><\/h2>\n<p>Not every project needs fully automated VPS builds from day one. For a single small website, a manually configured VPS or a managed hosting solution is often enough. Automation starts to shine when:<\/p>\n<ul>\n<li>You manage multiple environments (development, staging, production).<\/li>\n<li>You host several projects or many clients on dchost VPS or dedicated servers.<\/li>\n<li>You need to scale horizontally during campaigns or seasonal peaks.<\/li>\n<li>You must comply with strict security\/audit requirements and prove how servers are configured.<\/li>\n<\/ul>\n<p>The beauty of Terraform + Ansible is that the same patterns apply whether the underlying compute is a single dchost VPS, a farm of VPS instances, a dedicated server or your own hardware in our colocation facilities. Once you invest in infrastructure as code, migrating between these options, or scaling up over time, becomes far less painful.<\/p>\n<p>If you are unsure which base platform\u2014VPS versus dedicated\u2014is the best starting point for your Terraform + Ansible stack, our comparison on <a href=\"https:\/\/www.dchost.com\/blog\/en\/dedicated-sunucu-mu-vps-mi-hangisi-isinize-yarar\/\">choosing between dedicated servers and VPS for your business<\/a> can help clarify the trade-offs.<\/p>\n<h2><span id=\"Bringing_It_All_Together\">Bringing It All Together<\/span><\/h2>\n<p>Automating VPS setup with Terraform and Ansible is not about fancy tooling for its own sake; it is about turning fragile, one-off server builds into a reliable, repeatable process. Terraform declares and provisions your VPS instances and networking; Ansible turns them into hardened, monitored, backed-up application servers. Together, they give you a push-button way to create or recreate your infrastructure\u2014whether you are bringing up a new staging environment, adding capacity for a campaign, or recovering after a hardware failure.<\/p>\n<p>As the dchost.com team, we see customers gain a lot of confidence once their hosting stack becomes code they can read, review and version-control. If you want to adopt this approach on your own dchost VPS, dedicated server or colocation environment, you can start small: one Terraform module, one Ansible role, and a simple Makefile. From there, you can grow into DNS automation, SSL, monitoring and backups using the resources we linked throughout this article.<\/p>\n<p>If you would like help choosing the right VPS or server configuration for your Terraform + Ansible setup, or you want to discuss how to align this with your backup, monitoring and security requirements, our team at dchost.com is ready to walk through real scenarios with you and design an infrastructure that is both powerful and maintainable.<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"<p>If you find yourself repeating the same VPS setup steps again and again\u2014creating users, hardening SSH, installing Docker or a web stack, opening the same ports\u2014it is time to treat your servers like code. With Terraform and Ansible, you can turn the entire VPS lifecycle into a predictable pipeline: provision the machine, configure the OS [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":3638,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[26],"tags":[],"class_list":["post-3637","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-teknoloji"],"_links":{"self":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/3637","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/comments?post=3637"}],"version-history":[{"count":0,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/3637\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media\/3638"}],"wp:attachment":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media?parent=3637"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/categories?post=3637"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/tags?post=3637"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}