Continuous integration (CI) makes the cycle from design to code to building artifacts seamless and consistent. Continuous delivery (CD) makes delivery of that artifact to an environment the same every time.
But, what about the actual environment the artifact is running in? Is it the same every time?
That’s a hard thing to guarantee — unless you take advantage of an Infrastructure-as-Code (IaC) approach. This post explains how to use Infrastructure-as-Code to improve CI/CD. We’ll use Terraform as our IaC tool, although the lessons below could be applied using any Infrastructure-as-Code solution.
What is Infrastructure-as-Code? The basic idea of Infrastructure-as-Code is to have the configuration files required to stand up the environment your code runs in, along with the actual code in your company’s source code management tool. Then, as part of the deployment process, your infrastructure automation tool of choice (e.g., Terraform) will build what is required as a step in the process. Then, your code will deploy on top of it. This allows all changes to the infrastructure to be tracked, along with the source code those files support, which allows for a truly reproducible deployment.
Using Terraform requires very basic steps. The first is to have your configuration files in their own directory structure. (Every organization has their own way to handle multiple environments – but for the purposes of this article, we’ll go with a flat directory structure.)
All configuration files are written in HashiCorp’s HCL format (which looks a lot like JSON) and it’s recommended they have the *.tf extension. The default file to load variables from is terraform.tfvars.
From a practical point of view, all these scripts could be in one file. But, I find that having everything separate makes maintenance easier as the project inevitably grows.
terraform.tfvars
project_name = "temporary-test-account" region = "us-central1" zone = "us-central1-a" cred_file = "~/serviceaccount.json" network_name = "terraform-example"
vars.tf
variable "project_name" { type = "string" } variable "region" { type = "string" } variable "zone" { type = "string" } variable "cred_file" { type = "string" } variable "network_name" { type = "string" }
gcp.tf creates a connection to a specific project and region within the Google Compute Platform:
provider "google" { credentials = "${file(var.cred_file)}" project = "${var.project_name}" region = "${var.region}" }
network.tf creates a custom network and subnet:
resource "google_compute_network" "vpc_network" { name = "${var.network_name}" auto_create_subnetworks = "false" } resource "google_compute_subnetwork" "vpc_subnet" { name = "${var.network_name}" ip_cidr_range = "10.222.0.0/20" network = "${google_compute_network.vpc_network.self_link}" region = "${var.region}" private_ip_google_access = true }
compute.tf creates a single VM in the subnet (specified above):
data "template_file" "metadata_startup_script" { template = "${file("bootstrap.sh")}" } resource "google_compute_instance" "vm_instance" { name = "terraform-instance" machine_type = "n1-standard-1" zone = "${var.zone}" boot_disk { initialize_params { image = "centos-cloud/centos-7" } } metadata_startup_script = "${data.template_file.metadata_startup_script.rendered}" network_interface { network = "${google_compute_network.vpc_network.self_link}" subnetwork = "${google_compute_subnetwork.vpc_subnet.self_link}" access_config = { } } }
firewall.tf opens ports 22 and 80 so we can connect and test:
resource "google_compute_firewall" "fw_access" { name = "terraform-firewall" network = "${google_compute_network.vpc_network.name}" allow { protocol = "icmp" } allow { protocol = "tcp" ports = ["22", "80"] } source_ranges = ["0.0.0.0/0"] }
bootstrap.sh in this case is loaded as a startup script in compute.tf and passed as a variable. By leveraging the template module, you can have the startup script (or any other data) be dynamically updated to include things like environment-specific database connection strings, or even a password from a vault (which HashiCorp also offers).
#!/bin/bash # This is used as the startup script by the Google compute unit # And will start an nginx container as an example # Update everything sudo yum -y update # Install Docker pre-reqs sudo yum -y install yum-utils device-mapper-persistent-data lvm2 # Remove any Docker installed by CentOS as a default sudo yum -y remove docker-client docker-common docker # Add the official Docker repo sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo # Install the official latest Docker Community Edition sudo yum -y install docker-ce # Enable and start the daemon sudo systemctl start docker sudo systemctl enable docker # Starting nginx as a container as it is easy and always works sudo docker run --name docker-nginx -p 80:80 -d nginx
The first step is checking out the Infrastructure-as-Code project from your source code repository and setting any default variables before you can initialize Terraform. (This working demo is available on GitHub.)
terraform-cicd:$ cp terraform.tfvars.example terraform.tfvars terraform-cicd:$ vi terraform.tfvars
Next, initialize Terraform, which downloads all the plugins required to use it:
terraform-cicd:$ terraform init
Initializing provider plugins...
The following providers do not have any version constraints in configuration, so the latest version was installed.
To prevent automatic upgrades to new major versions that may contain breaking changes, it is recommended to add version = "..." constraints to the corresponding provider blocks in configuration, with the constraint strings suggested below.
* provider.google: version = "~> 2.6" * provider.template: version = "~> 2.1"
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see any changes that are required for your infrastructure. All Terraform commands should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
The next required step in the latest version of Terraform is to apply, which creates the plan and executes it.
terraform-cicd:$ terraform apply data.template_file.metadata_startup_script: Refreshing state...
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
+ google_compute_firewall.fw_access id: <computed> allow.#: "2" allow.1367131964.ports.#: "0" allow.1367131964.protocol: "icmp" allow.186047796.ports.#: "2" allow.186047796.ports.0: "22" allow.186047796.ports.1: "80" allow.186047796.protocol: "tcp" creation_timestamp: <computed> destination_ranges.#: <computed> direction: <computed> name: "terraform-firewall" network: "terraform-example" priority: "1000" project: <computed> self_link: <computed> source_ranges.#: "1" source_ranges.1080289494: "0.0.0.0/0" + google_compute_instance.vm_instance id: <computed> boot_disk.#: "1" boot_disk.0.auto_delete: "true" boot_disk.0.device_name: <computed> boot_disk.0.disk_encryption_key_sha256: <computed> boot_disk.0.initialize_params.#: "1" boot_disk.0.initialize_params.0.image: "centos-cloud/centos-7" boot_disk.0.initialize_params.0.size: <computed> boot_disk.0.initialize_params.0.type: <computed> can_ip_forward: "false" cpu_platform: <computed> deletion_protection: "false" guest_accelerator.#: <computed> instance_id: <computed> label_fingerprint: <computed> machine_type: "n1-standard-1" metadata_fingerprint: <computed>
metadata_startup_script: "#!/bin/bash\n#
This is used as the startup script by the Google compute unit\n# And will start an nginx container as an example\n\n# Update everything\nsudo yum -y update\n\n# Install Docker pre-reqs\nsudo yum -y install yum-utils device-mapper-persistent-data lvm2\n\n# Remove any Docker installed by CentOS as a default\nsudo yum -y remove docker-client docker-common docker\n\n# Add the official Docker repo\nsudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo\n\n# Install the official latest Docker Community Edition\nsudo yum -y install docker-ce\n\n# Enable and start the daemon\nsudo systemctl start docker\nsudo systemctl enable docker\n\n# Starting nginx as a container as it is easy and always works\nsudo docker run --name docker-nginx -p 80:80 -d nginx\n\n" name: "terraform-instance" network_interface.#: "1" network_interface.0.access_config.#: "1" network_interface.0.access_config.0.assigned_nat_ip: <computed> network_interface.0.access_config.0.nat_ip: <computed> network_interface.0.access_config.0.network_tier: <computed> network_interface.0.address: <computed> network_interface.0.name: <computed> network_interface.0.network: "${google_compute_network.vpc_network.self_link}" network_interface.0.network_ip: <computed> network_interface.0.subnetwork_project: <computed> project: <computed> scheduling.#: <computed> self_link: <computed> tags_fingerprint: <computed> zone: <computed> + google_compute_network.vpc_network id: <computed> auto_create_subnetworks: "false" delete_default_routes_on_create: "false" gateway_ipv4: <computed> name: "terraform-example" project: <computed> routing_mode: <computed> self_link: <computed> + google_compute_subnetwork.vpc_subnet id: <computed> creation_timestamp: <computed> fingerprint: <computed> gateway_address: <computed> ip_cidr_range: "10.222.0.0/24" name: "terraform-example" network: "${google_compute_network.vpc_network.self_link}" private_ip_google_access: "true" project: <computed> region: "us-central1-a" secondary_ip_range.#: <computed> self_link: <computed> Plan: 4 to add, 0 to change, 0 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes google_compute_network.vpc_network: Creating... … … … google_compute_instance.vm_instance: Creation complete after 49s (ID: terraform-instance) Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
Did it work?
terraform-cicd:$ gcloud compute instances list | egrep 'EXTERNAL_IP|terraform-instance' NAME ZONE MACHINE_TYPE PREEMPTIBLE INTERNAL_IP EXTERNAL_IP STATUS terraform-instance us-central1-a n1-standard-1 10.222.0.2 35.192.156.240 RUNNING terraform-cicd:$ curl http://35.192.156.240 <!DOCTYPE html> … … </html>
Yes it did! And now that we’ve tested it, it can go away.
terraform-cicd:$ terraform destroy google_compute_network.vpc_network: Refreshing state... (ID: terraform-example) data.template_file.metadata_startup_script: Refreshing state... google_compute_firewall.fw_access: Refreshing state... (ID: terraform-firewall) google_compute_subnetwork.vpc_subnet: Refreshing state... (ID: us-central1/terraform-example) google_compute_instance.vm_instance: Refreshing state... (ID: terraform-instance)
An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: - destroy Terraform will perform the following actions: - google_compute_firewall.fw_access - google_compute_instance.vm_instance - google_compute_network.vpc_network - google_compute_subnetwork.vpc_subnet Plan: 0 to add, 0 to change, 4 to destroy. Do you really want to destroy all resources? Terraform will destroy all your managed infrastructure, as shown above. There is no undo. Only 'yes' will be accepted to confirm. Enter a value: yes
google_compute_firewall.fw_access: Destroying... (ID: terraform-firewall) google_compute_instance.vm_instance: Destroying... (ID: terraform-instance) google_compute_firewall.fw_access: Destruction complete after 8s google_compute_instance.vm_instance: Still destroying... (ID: terraform-instance, 10s elapsed) … … google_compute_instance.vm_instance: Destruction complete after 2m10s google_compute_subnetwork.vpc_subnet: Destroying... (ID: us-central1/terraform-example) google_compute_subnetwork.vpc_subnet: Still destroying... (ID: us-central1/terraform-example, 10s elapsed) google_compute_subnetwork.vpc_subnet: Still destroying... (ID: us-central1/terraform-example, 20s elapsed) google_compute_subnetwork.vpc_subnet: Destruction complete after 27s google_compute_network.vpc_network: Destroying... (ID: terraform-example) google_compute_network.vpc_network: Still destroying... (ID: terraform-example, 10s elapsed) google_compute_network.vpc_network: Still destroying... (ID: terraform-example, 20s elapsed) google_compute_network.vpc_network: Destruction complete after 27s
Destroy complete! Resources: 4 destroyed.
From popular services like Circle CI to the ever-present Jenkins, regardless of which tool you use for CI/CD, it either already has a plugin for Terraform or you can simply run it from the command line, as detailed above.
By leveraging environment creation and cleanup as part of your CI/CD pipelines, you can reduce the number of incidents that occur as deployments move between environments, as everything is generated from developer-supported templates. Any events that are generated as part of a build can easily be tagged and routed through your incident management platform, and end up with the development team supporting that application service.
Maintain a fast, reliable CI/CD pipeline and respond to incidents during deployment with Splunk Observability Cloud. Sign up for a 14-day free trial to see how DevOps teams are maintaining CI/CD and making on-call suck less with a holistic incident management and real-time response solution.
Vince Power is a Solution Architect who has a focus on cloud adoption and technology implementations using open source-based technologies. He has extensive experience with core computing and networking (IaaS), identity and access management (IAM), application platforms (PaaS), and continuous delivery.
The Splunk platform removes the barriers between data and action, empowering observability, IT and security teams to ensure their organizations are secure, resilient and innovative.
Founded in 2003, Splunk is a global company — with over 7,500 employees, Splunkers have received over 1,020 patents to date and availability in 21 regions around the world — and offers an open, extensible data platform that supports shared data across any environment so that all teams in an organization can get end-to-end visibility, with context, for every interaction and business process. Build a strong data foundation with Splunk.