Monthly Archives: November 2019

Configure ACI with Terraform and Gitlab CI/CD

Introduction

As promised, we’ll configure an ACI network using a CI/CD pipeline. If you understood this post, you’ll easily grasp this one as well. In the end, whether you configure an ACI resource or an ACI resource with Gitlab, the same principles apply. So we’ll run fast over this one.

In this blog post, we will be using exactly the same Terraform files as we used in the previous post so I won’t spend time describing them here.

Gitlab configuration

In this post, we used the online version of Gitlab. For this post, we will be installing our own Gitlab server since I cannot access our internal lab from outside. Hence the need to install the Community Edition of Gitlab in my lab. I won’t go over the installation in this blog post. A pretty easy guide can be found here.

Anyway, after installation, continue to create a project.

Once the project has been created, you will see the below page.

So let’s go ahead and add the files to our repository. Just follow the commands below.

cisco@wauterw-main:~/wim/terraform/ACI$ git init
Initialized empty Git repository in /home/cisco/wim/terraform/ACI/.git/
cisco@wauterw-main:~/wim/terraform/ACI$ git remote add origin http://10.16.2.230/cisco/terraform-aci.git
cisco@wauterw-main:~/wim/terraform/ACI$ git add .
cisco@wauterw-main:~/wim/terraform/ACI$ git commit -m "Initial Commit"
[master (root-commit) 7c7a905] Initial Commit
 4 files changed, 37 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 main.tf
 create mode 100644 terraform.tf
 create mode 100644 variables.tf
cisco@wauterw-main:~/wim/terraform/ACI$ git push -u origin master
Username for 'http://10.16.2.230': wauterw@cisco.com
Password for 'http://wauterw@cisco.com@10.16.2.230':
Counting objects: 6, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (6/6), 747 bytes | 747.00 KiB/s, done.
Total 6 (delta 0), reused 0 (delta 0)
To http://10.16.2.230/cisco/terraform-aci.git
 * [new branch]      master -> master
Branch 'master' set up to track remote branch 'master' from 'origin'.

Your files will be added to the repository now.

Gitlab CI/CD

As we learned in earlier posts, we need to create a ‘.gitlab-ci.yaml’ with the following content. It does define a number of stagesm essentially to do a ‘terraform init’, ‘terraform plan’ and ‘terraform apply’.

image:
  name: hashicorp/terraform:light
  entrypoint:
    - '/usr/bin/env'
    - 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'

before_script:
  - rm -rf .terraform
  - terraform --version
  - export AWS_ACCESS_KEY_ID
  - export AWS_SECRET_ACCESS_KEY
  - terraform init

stages:
  - validate
  - plan
  - apply

validate:
  stage: validate
  script:
    - terraform validate

plan:
  stage: plan
  script:
    - terraform plan -out "planfile"
  dependencies:
    - validate
  artifacts:
    paths:
      - planfile

apply:
  stage: apply
  script:
    - terraform apply -input=false "planfile"
  dependencies:
    - plan
  when: manual

Save the .gitlab-ci.yaml into the same folder as the rest of your Terraform files.

cisco@wauterw-ubuntu-desktop:~/software/Terraform/ACI_Tenant_VRF_BD$ git add . 
cisco@wauterw-ubuntu-desktop:~/software/Terraform/ACI_Tenant_VRF_BD$ git commit -m "Adding CI/CD config"
[master 96adfe7] Adding CI/CD config
 1 file changed, 40 insertions(+)
 create mode 100644 .gitlab-ci.yml
cisco@wauterw-ubuntu-desktop:~/software/Terraform/ACI_Tenant_VRF_BD$ git push origin master
Username for 'http://10.16.2.230': cisco
Password for 'http://cisco@10.16.2.230': 
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 4 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 576 bytes | 576.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0)
To http://10.16.2.230/cisco/terraform-aci.git
   eb4693c..96adfe7  master -> master

After this is done, your repo will look as follows:

Once Gitlab detects that a .gitlab-ci.yml file is uploaded to your repo, it will automatically start the pipeline process.

And as we put in the .gitlab-ci.yml file that we wanted to manually approve before applying the configuration, you will see that the pipeline stops after the two first steps of the pipeline. Waiting until you approve manually. Once approved you will see that all three steps are completed.

And finally, you will also see the configuration on Cisco’s ACI platform.

Destroy ACI configuration

Let’s now use a pipeline to destroy the network constructs on ACI automatically.

image:
  name: hashicorp/terraform:light
  entrypoint:
    - '/usr/bin/env'
    - 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'

before_script:
  - rm -rf .terraform
  - terraform --version
  - export AWS_ACCESS_KEY_ID
  - export AWS_SECRET_ACCESS_KEY
  - terraform init

stages:
  - validate
  - destroy

validate:
  stage: validate
  script:
    - terraform validate

destroy:
  stage: destroy
  script:
    - terraform destroy -auto-approve
  when: manual
cisco@wauterw-ubuntu-desktop:~/software/Terraform/ACI_Tenant_VRF_BD$ git add .gitlab-ci.yml 
cisco@wauterw-ubuntu-desktop:~/software/Terraform/ACI_Tenant_VRF_BD$ git commit -m "Adding new CI/CD config"
[master 31b894f] Adding new CI/CD config
 1 file changed, 11 deletions(-)
cisco@wauterw-ubuntu-desktop:~/software/Terraform/ACI_Tenant_VRF_BD$ git push origin master
Username for 'http://10.16.2.230': cisco
Password for 'http://cisco@10.16.2.230': 
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 4 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 299 bytes | 299.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0)
To http://10.16.2.230/cisco/terraform-aci.git
   c28c1ed..31b894f  master -> master

As you can see from below screenshot, we only have two steps now, the validate and destroy step.

Click on the trigger to destroy the network configuration on Cisco ACI. When all works well, you will see the below screen:

And obviously, the tenant, VRF and BD will be removed from the ACI network.

Configure ACI with Terraform

Introduction

In this post, we created some EC2 instances on AWS. In this post, we will apply the same principle but instead of creating some servers on AWS, we will create a network on Cisco’s ACI solution using Terraform’s ACI provider. If you want to learn more on ACI, read this and this.

Terraform code

We will create a provider.tf file which contains nothing more than the ACI configuration. The URL is the URL for your APIC. I will take the fast route and put insecure to true, but you can also use certificate. It’s all in the documentation.

provider "aci" {
  username = "admin"
  password = "***"
  url      = "https://10.x.y.1"
  insecure = true
}

resource "aci_tenant" "Tenant_TF_Demo" {
  name        = var.aci_tenant
  description = "Tenant created by TF"
}

resource "aci_vrf" "VRF_TF_Demo" {
  tenant_dn = aci_tenant.Tenant_TF_Demo.id
  name      = var.aci_vrf
}

resource "aci_bridge_domain" "BD_TF_Demo" {
  tenant_dn          = aci_tenant.Tenant_TF_Demo.id
  name               = var.aci_bd"
  description        = "BD created by TF"
  relation_fv_rs_ctx = aci_vrf.VRF_TF_Demo.name
}

I will create a new tenant, a VRF under that tenant and a BD under that VRF. For this blogpost I will limit myself to these three network constructs just to give you an idea of how things work. In a later blog post (when I have more time), I will create a more elaborate infrastructure.

We will be reading the information from a Terraform variable file.

variable "aci_tenant" {
  default = "Tenant_Terraform_Demo"
}

variable "aci_vrf" {
  default = "VRF_Terraform_Demo"
}

variable "aci_bd" {
  default = "BD_Terraform_Demo"
}

Note: since Terraform 0.12, there is no need for putting quotes around the types. Terraform will give you a warning as follows:

Terraform 0.11 and earlier required type constraints to be given in quotes,
but that form is now deprecated and will be removed in a future version of
Terraform. To silence this warning, remove the quotes around “string”.

The easiest way to transform all you Terraform 0.11 files to version 0.12 is to execute the following:

cisco@wauterw-ubuntu-desktop:~/software/Terraform/ACI_Tenant_VRF_BD$ terraform 0.12upgrade

This command will rewrite the configuration files in the given directory so
that they use the new syntax features from Terraform v0.12, and will identify
any constructs that may need to be adjusted for correct operation with
Terraform v0.12.

We recommend using this command in a clean version control work tree, so that
you can easily see the proposed changes as a diff against the latest commit.
If you have uncommited changes already present, we recommend aborting this
command and dealing with them before running this command again.

Would you like to upgrade the module in the current directory?
  Only 'yes' will be accepted to confirm.

  Enter a value: yes

-----------------------------------------------------------------------------

Upgrade complete!

The configuration files were upgraded successfully. Use your version control
system to review the proposed changes, make any necessary adjustments, and
then commit.

Deploy infrastructure

As we did with AWS resources (see here), we will follow exactly the same pattern.

First we will perform ‘terraform init’. This will essentially download the ACI provider from Terraform repo.

cisco@wauterw-main:~/wim/terraform/ACI$ terraform init

Initializing the backend...

Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.

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.aci: version = "~> 0.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.

You see in above snippet, it’s configuring the S3 backend (I chose to use a shared tfstate file) and it downloads the ACI provider. Next, we’ll perform a ‘terraform plan’ to see what will be created.

cisco@wauterw-ubuntu-desktop:~/software/Terraform/ACI_Tenant_VRF_BD$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

aci_tenant.Tenant_TF_Demo: Refreshing state... [id=uni/tn-Tenant_Terraform_Demo]
aci_vrf.VRF_TF_Demo: Refreshing state... [id=uni/tn-Tenant_Terraform_Demo/ctx-VRF_Terraform_Demo]

------------------------------------------------------------------------

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:

  # aci_bridge_domain.BD_TF_Demo will be created
  + resource "aci_bridge_domain" "BD_TF_Demo" {
      + annotation                  = (known after apply)
      + arp_flood                   = (known after apply)
      + bridge_domain_type          = (known after apply)
      + description                 = "BD created by TF"
      + ep_clear                    = (known after apply)
      + ep_move_detect_mode         = (known after apply)
      + host_based_routing          = (known after apply)
      + id                          = (known after apply)
      + intersite_bum_traffic_allow = (known after apply)
      + intersite_l2_stretch        = (known after apply)
      + ip_learning                 = (known after apply)
      + ipv6_mcast_allow            = (known after apply)
      + limit_ip_learn_to_subnets   = (known after apply)
      + ll_addr                     = (known after apply)
      + mac                         = (known after apply)
      + mcast_allow                 = (known after apply)
      + multi_dst_pkt_act           = (known after apply)
      + name                        = "BD_Terraform_Demo"
      + name_alias                  = (known after apply)
      + optimize_wan_bandwidth      = (known after apply)
      + relation_fv_rs_ctx          = "VRF_Terraform_Demo"
      + tenant_dn                   = "uni/tn-Tenant_Terraform_Demo"
      + unicast_route               = (known after apply)
      + unk_mac_ucast_act           = (known after apply)
      + unk_mcast_act               = (known after apply)
      + v6unk_mcast_act             = (known after apply)
      + vmac                        = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

Next, let’s apply the configuration. You should know the drill by now 🙂

cisco@wauterw-ubuntu-desktop:~/software/Terraform/ACI_Tenant_VRF_BD$ terraform apply

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:

  # aci_bridge_domain.BD_TF_Demo will be created
  + resource "aci_bridge_domain" "BD_TF_Demo" {
      + annotation                  = (known after apply)
      + arp_flood                   = (known after apply)
      + bridge_domain_type          = (known after apply)
      + description                 = "BD created by TF"
      + ep_clear                    = (known after apply)
      + ep_move_detect_mode         = (known after apply)
      + host_based_routing          = (known after apply)
      + id                          = (known after apply)
      + intersite_bum_traffic_allow = (known after apply)
      + intersite_l2_stretch        = (known after apply)
      + ip_learning                 = (known after apply)
      + ipv6_mcast_allow            = (known after apply)
      + limit_ip_learn_to_subnets   = (known after apply)
      + ll_addr                     = (known after apply)
      + mac                         = (known after apply)
      + mcast_allow                 = (known after apply)
      + multi_dst_pkt_act           = (known after apply)
      + name                        = "BD_Terraform_Demo"
      + name_alias                  = (known after apply)
      + optimize_wan_bandwidth      = (known after apply)
      + relation_fv_rs_ctx          = "VRF_Terraform_Demo"
      + tenant_dn                   = (known after apply)
      + unicast_route               = (known after apply)
      + unk_mac_ucast_act           = (known after apply)
      + unk_mcast_act               = (known after apply)
      + v6unk_mcast_act             = (known after apply)
      + vmac                        = (known after apply)
    }

  # aci_tenant.Tenant_TF_Demo will be created
  + resource "aci_tenant" "Tenant_TF_Demo" {
      + annotation  = (known after apply)
      + description = "Tenant created by TF"
      + id          = (known after apply)
      + name        = "Tenant_Terraform_Demo"
      + name_alias  = (known after apply)
    }

  # aci_vrf.VRF_TF_Demo will be created
  + resource "aci_vrf" "VRF_TF_Demo" {
      + annotation             = (known after apply)
      + bd_enforced_enable     = (known after apply)
      + id                     = (known after apply)
      + ip_data_plane_learning = (known after apply)
      + knw_mcast_act          = (known after apply)
      + name                   = "VRF_Terraform_Demo"
      + name_alias             = (known after apply)
      + pc_enf_dir             = (known after apply)
      + pc_enf_pref            = (known after apply)
      + tenant_dn              = (known after apply)
    }

Plan: 3 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

aci_tenant.Tenant_TF_Demo: Creating...
aci_tenant.Tenant_TF_Demo: Creation complete after 1s [id=uni/tn-Tenant_Terraform_Demo]
aci_vrf.VRF_TF_Demo: Creating...
aci_vrf.VRF_TF_Demo: Creation complete after 1s [id=uni/tn-Tenant_Terraform_Demo/ctx-VRF_Terraform_Demo]
aci_bridge_domain.BD_TF_Demo: Creating...
aci_bridge_domain.BD_TF_Demo: Creation complete after 1s [id=uni/tn-Tenant_Terraform_Demo/BD-BD_Terraform_Demo]

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Log into ACI and see that the tenant is created there:

Also the VRF and the BD are created as you can see in below screenshot:

Pretty neat stuff.

Let’s continue to delete what we created by issuing a ‘terraform destroy’.

cisco@wauterw-ubuntu-desktop:~/software/Terraform/ACI_Tenant_VRF_BD$ terraform destroy
aci_tenant.Tenant_TF_Demo: Refreshing state... [id=uni/tn-Tenant_Terraform_Demo]
aci_vrf.VRF_TF_Demo: Refreshing state... [id=uni/tn-Tenant_Terraform_Demo/ctx-VRF_Terraform_Demo]
aci_bridge_domain.BD_TF_Demo: Refreshing state... [id=uni/tn-Tenant_Terraform_Demo/BD-BD_Terraform_Demo]

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:

  # aci_bridge_domain.BD_TF_Demo will be destroyed
  - resource "aci_bridge_domain" "BD_TF_Demo" {
      - arp_flood                   = "no" -> null
      - bridge_domain_type          = "regular" -> null
      - description                 = "BD created by TF" -> null
      - ep_clear                    = "no" -> null
      - host_based_routing          = "no" -> null
      - id                          = "uni/tn-Tenant_Terraform_Demo/BD-BD_Terraform_Demo" -> null
      - intersite_bum_traffic_allow = "no" -> null
      - intersite_l2_stretch        = "no" -> null
      - ip_learning                 = "yes" -> null
      - limit_ip_learn_to_subnets   = "yes" -> null
      - ll_addr                     = "::" -> null
      - mac                         = "00:22:BD:F8:19:FF" -> null
      - mcast_allow                 = "no" -> null
      - multi_dst_pkt_act           = "bd-flood" -> null
      - name                        = "BD_Terraform_Demo" -> null
      - optimize_wan_bandwidth      = "no" -> null
      - relation_fv_rs_ctx          = "VRF_Terraform_Demo" -> null
      - tenant_dn                   = "uni/tn-Tenant_Terraform_Demo" -> null
      - unicast_route               = "yes" -> null
      - unk_mac_ucast_act           = "proxy" -> null
      - unk_mcast_act               = "flood" -> null
      - vmac                        = "not-applicable" -> null
    }

  # aci_tenant.Tenant_TF_Demo will be destroyed
  - resource "aci_tenant" "Tenant_TF_Demo" {
      - description = "Tenant created by TF" -> null
      - id          = "uni/tn-Tenant_Terraform_Demo" -> null
      - name        = "Tenant_Terraform_Demo" -> null
    }

  # aci_vrf.VRF_TF_Demo will be destroyed
  - resource "aci_vrf" "VRF_TF_Demo" {
      - bd_enforced_enable     = "no" -> null
      - id                     = "uni/tn-Tenant_Terraform_Demo/ctx-VRF_Terraform_Demo" -> null
      - ip_data_plane_learning = "enabled" -> null
      - knw_mcast_act          = "permit" -> null
      - name                   = "VRF_Terraform_Demo" -> null
      - pc_enf_dir             = "ingress" -> null
      - pc_enf_pref            = "enforced" -> null
      - tenant_dn              = "uni/tn-Tenant_Terraform_Demo" -> null
    }

Plan: 0 to add, 0 to change, 3 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

aci_bridge_domain.BD_TF_Demo: Destroying... [id=uni/tn-Tenant_Terraform_Demo/BD-BD_Terraform_Demo]
aci_bridge_domain.BD_TF_Demo: Destruction complete after 0s
aci_vrf.VRF_TF_Demo: Destroying... [id=uni/tn-Tenant_Terraform_Demo/ctx-VRF_Terraform_Demo]
aci_vrf.VRF_TF_Demo: Destruction complete after 0s
aci_tenant.Tenant_TF_Demo: Destroying... [id=uni/tn-Tenant_Terraform_Demo]
aci_tenant.Tenant_TF_Demo: Destruction complete after 0s

Destroy complete! Resources: 3 destroyed.

As you can see, the tenant and all underlying objects have been destroyed.

Hope you had some fun, next blog post we’ll be integrating this in a CI/CD pipeline.

Ciao!

Configure AWS instances using Terraform and Gitlab CI/CD

Introduction

In this post, we will deploy EC2 instances on AWS using a CI/CD pipeline. If you want to follow along, it’s advised you also refer to part 1 and part 2 for getting some background information.

Code

Below are the files we are going to use. If you went through part 1 and part 2 you’ll surely know what they are doing. Won’t explain them here anymore, but will just list them.

provider.tf:

provider "aws" {
  version    = "~> 2.0"
  access_key = "${var.ec2_aws_access_key}"
  secret_key = "${var.ec2_aws_secret_key}"
  region     = "${var.ec2_region}"
}

variables.tf:

variable "ec2_aws_access_key" {
  default = "AK***A"
}

variable "ec2_aws_secret_key" {
  default = "GD***S7V"
}

variable "ec2_region" {
  default = "eu-west-1"
}

variable "ec2_image" {
  default = "ami-00035f41c82244dab"
}

variable "ec2_instance_type" {
  default = "t2.micro"
}

variable "ec2_keypair" {
  default = "AWS-Cisco"
}
variable "ec2_tags" {
  default = "Cisco-Demo-Terraform-1"
}

main.tf:

resource "aws_instance" "OneServer" {
  ami           = "${var.ec2_image}"
  instance_type = "${var.ec2_instance_type}"
  key_name      = "${var.ec2_keypair}"
  count         = "${var.ec2_count}"
  tags = {
    Name = "${var.ec2_tags}"
  }
}

output "instance_ip_addr" {
  value       = "${aws_instance.OneServer.*.private_ip}"
  description = "The private IP address of the main server instance."
}

output "instance_ips" {
  value = "${aws_instance.OneServer.*.public_ip}"
}

terraform.tf (remote state):

terraform {
  backend "s3" {
    bucket = "be.wymedia.terraform"
    key    = "terraform/state"
    region = "eu-west-1"
  }
}

Create Gitlab repository

We will be using Gitlab in this post. I’m going to assume you have a working Gitlab application. For this post, you can just go to http://gitlab.com and create an account. It’s free.

Next login and create a new project. See the screenshot below:

Once the project is created, you will get the below screen:

Next, go to the folder where you stored all the tf files and just follow what Gitlab informed you about, dead easy. Below are the commands you should use:

WAUTERW-M-65P7:_Create_2_EC2_instance_3_files_cicd wauterw$ git init
Initialized empty Git repository in /Users/wauterw/Dropbox/Programming/Terraform/AWS/_Create_2_EC2_instance_3_files_cicd/.git/
WAUTERW-M-65P7:_Create_2_EC2_instance_3_files_cicd wauterw$ git add .
WAUTERW-M-65P7:_Create_2_EC2_instance_3_files_cicd wauterw$ git remote add origin https://gitlab.com/wiwa1978/terraform_aws_cicd.git
WAUTERW-M-65P7:_Create_2_EC2_instance_3_files_cicd wauterw$ git commit -m "Initial commit"
[master (root-commit) 353719b] Initial commit
 5 files changed, 69 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 main.tf
 create mode 100644 provider.tf
 create mode 100644 terraform.tf
 create mode 100644 variables.tf
WAUTERW-M-65P7:_Create_2_EC2_instance_3_files_cicd wauterw$ git push origin master
Username for 'https://gitlab.com': wauters1978@gmail.com
Password for 'https://wauters1978@gmail.com@gitlab.com': 
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 12 threads
Compressing objects: 100% (6/6), done.
Writing objects: 100% (7/7), 1.01 KiB | 1.01 MiB/s, done.
Total 7 (delta 0), reused 0 (delta 0)
To https://gitlab.com/wiwa1978/terraform_aws_cicd.git
 * [new branch]      master -> master

Eventually, your repository on Gitlab will contain all your files.

Configure Gitlab pipeline

>
Now that all files are in your git repository, we are going to use Gitlab’s CI/CD functionality. To do so, go to Settings > CI/CD first. Go to the variables section and add your AWS credentials over there. See below screenshot.

Next, we will need to create the pipeline we want to execute. In our case, we will create a pipeline which contains all the stages to execute the Terraform functions.

We will start with pulling the hashicorp/terraform image. In the background, Gitlab will create a docker container (running that image). Next, you will notice that we specify two export statements. Essentially, these statements will take the environment variables we created above and add them to your docker instance.

The rest of the file is pretty self-explanatory in my view. We will validate our TF files, we will make a TF plan and we will apply the configuration. We have added the ‘when: manual’ statement to the apply stage because we want Gitlab to ask for our confirmation before deploying our infrastructure.

image:
  name: hashicorp/terraform:light
  entrypoint:
    - '/usr/bin/env'
    - 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'

before_script:
  - rm -rf .terraform
  - terraform --version
  - export AWS_ACCESS_KEY_ID
  - export AWS_SECRET_ACCESS_KEY
  - terraform init

stages:
  - validate
  - plan
  - apply

validate:
  stage: validate
  script:
    - terraform validate

plan:
  stage: plan
  script:
    - terraform plan -out "planfile"
  dependencies:
    - validate
  artifacts:
    paths:
      - planfile

apply:
  stage: apply
  script:
    - terraform apply -input=false "planfile"
  dependencies:
    - plan
  when: manual

It’s important you call the above file ‘.gitlab-ci.yml’.

Execute Gitlab pipeline

Once you commit this file to your Gitlab repository, Gitlab will automatically (by default) start the execution of your pipeline.

So we need to commit this file to our repository. You can do this using below commands:

WAUTERW-M-65P7:_Create_2_EC2_instance_3_files_cicd wauterw$ git add .gitlab-ci.yml 
WAUTERW-M-65P7:_Create_2_EC2_instance_3_files_cicd wauterw$ git commit -m "Adding CICD config"
[master c234b6e] Adding CICD config
 1 file changed, 40 insertions(+)
 create mode 100644 .gitlab-ci.yml
WAUTERW-M-65P7:_Create_2_EC2_instance_3_files_cicd wauterw$ git push origin master
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 12 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 570 bytes | 570.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0)
To https://gitlab.com/wiwa1978/terraform_aws_cicd.git
   353719b..c234b6e  master -> master

Note: Make sure the remote state is cleaned in the beginning, otherwise you might run into some issues.

In Gitlab, go to CI/CD pipelines and you will visually see the progress as Gitlab walks through the pipeline configuration. You’ll see that Gitlabs first will execute the validate step and when that was successful moves to the plan stage.

The pipeline executing will stop after the first two steps as we asked for a manual confirmation before continuing the deployment.

If the pipeline has finished without errors, you will see the servers in your AWS account.

Changing the infrastructure

Let’s give our instances a different name. Add the below snippet to your main.tf file.

tags = {
    Name = "${var.ec2_tags}"
  }

and the below to your variables.tf file:

variable "ec2_tags" {
  default = "Cisco-Demo-Terraform-new"
}

Once done, commit these changes to your Gitlab repository using the below commands.

WAUTERW-M-65P7:_Create_2_EC2_instance_3_files_cicd wauterw$ git add .
WAUTERW-M-65P7:_Create_2_EC2_instance_3_files_cicd wauterw$ git commit -m "Change name" 
[master 024b70c] Change name
 1 file changed, 1 insertion(+), 1 deletion(-)
WAUTERW-M-65P7:_Create_2_EC2_instance_3_files_cicd wauterw$ git push origin master
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 12 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 294 bytes | 294.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0)
To https://gitlab.com/wiwa1978/terraform_aws_cicd.git
   c234b6e..024b70c  master -> master

Check your pipeline execution in Gitlab and verify that it went successfully through all steps

And obviously, all changes will have been propagated to your AWS instances.

Pretty neat isn’t it. Gitlab is just a brilliant tool and makes CI/CD a breeze.

See you next time!

Configure AWS instances with Terraform: remote state

Introduction

In an earlier post, we created one EC2 instance. As explained in that post, the Terraform state file was created in your local folder. That’s great if you are the only person in need of the infrastructure you manage. In reality though, teams are sharing and managing the infrastructure. So we need a way to share the state file with other team members, so at all times they have to know the latest state of the infrastructure.

Hence, in this blogpost, we are going to explore a way to share the state file amongst team members. There are multiple ways to achieve this, but I tend to prefer storing the state file in an AWS S3 bucket.

Configure the remote state location

As mentioned, we will use an S3 bucket to store the state file. To do so, create a bucket on S3 and give it the below permissions (in my case the bucket is called be.wymedia.terraform.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::be.wymedia.terraform"
        },
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:GetObject",
                "s3:PutObject"
            ],
            "Resource": "arn:aws:s3:::be.wymedia.terraform/terraform/state"
        }
    ]
}

Once this is done on AWS, go back to your Terraform directory and configure the remote state location. You can do it as follows.

terraform {
  backend "s3" {
    bucket = "be.wymedia.terraform"
    key    = "terraform/state"
    region = "eu-west-1"
  }
}

What follows are essentially the same Terraform files as used in the previous post with a small change. We are going to configure 2 instances onto AWS.

The provider.tf file:

provider "aws" {
  version    = "~> 2.0"
  access_key = "${var.ec2_aws_access_key}"
  secret_key = "${var.ec2_aws_secret_key}"
  region     = "${var.ec2_region}"
}

The variables.tf file:

variable "ec2_aws_access_key" {
  default = "AKIA***Z5A"
}

variable "ec2_aws_secret_key" {
  default = "GDl***S7V"
}

variable "ec2_region" {
  default = "eu-west-1"
}

variable "ec2_image" {
  default = "ami-00035f41c82244dab"
}

variable "ec2_instance_type" {
  default = "t2.micro"
}

variable "ec2_keypair" {
  default = "AWS-Cisco"
}
variable "ec2_tags" {
  default = "Cisco-Demo-Terraform"
}

variable "ec2_count" {
  default = 2
}

The main.tf file:

resource "aws_instance" "OneServer" {
  ami           = "${var.ec2_image}"
  instance_type = "${var.ec2_instance_type}"
  key_name      = "${var.ec2_keypair}"
  count         = "${var.ec2_count}"
}

output "instance_ip_addr" {
  value       = "${aws_instance.OneServer.*.private_ip}"
  description = "The private IP address of the main server instance."
}

output "instance_ips" {
  value = "${aws_instance.OneServer.*.public_ip}"
}

Deployment

First off, we start with ‘terraform init’.

WAUTERW-M-65P7:_Create_2_EC2_instance_3_files_remotestate wauterw$ terraform init

Initializing the backend...

Error: No valid credential sources found for AWS Provider.
        Please see https://terraform.io/docs/providers/aws/index.html for more information on
        providing credentials for the AWS Provider

As you can see this does not work. For the ‘terraform init’ to succeed, it’s important you also set the correct credentials in your ~/.aws/credentials file. If not, it will fail with a ‘No valid credential sources found for AWS Provider’ error. Don’t really understand it as we specified the credentials in the Terraform file but it’s something I came across and wanted to share.

[default]
aws_access_key_id = AK***5A
aws_secret_access_key = GD***7V

Let’s retry the ‘terraform init’.

WAUTERW-M-65P7:_Create_2_EC2_instance_3_files_remotestate wauterw$ terraform init

Initializing the backend...

Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...
- Checking for available provider plugins...
- Downloading plugin for provider "aws" (hashicorp/aws) 2.39.0...

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.

Next, issue the ‘terraform plan’ command

WAUTERW-M-65P7:_Create_2_EC2_instance_3_files_remotestate wauterw$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
------------------------------------------------------------------------

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:

  # aws_instance.OneServer[0] will be created
  + resource "aws_instance" "OneServer" {
      + ami                          = "ami-00035f41c82244dab"
      + arn                          = (known after apply)
      + associate_public_ip_address  = (known after apply)
      + availability_zone            = (known after apply)
      + cpu_core_count               = (known after apply)
      + cpu_threads_per_core         = (known after apply)
      + get_password_data            = false
      + host_id                      = (known after apply)
      + id                           = (known after apply)
      + instance_state               = (known after apply)
      + instance_type                = "t2.micro"
      + ipv6_address_count           = (known after apply)
      + ipv6_addresses               = (known after apply)
      + key_name                     = "AWS-Cisco"
      + network_interface_id         = (known after apply)
      + password_data                = (known after apply)
      + placement_group              = (known after apply)
      + primary_network_interface_id = (known after apply)
      + private_dns                  = (known after apply)
      + private_ip                   = (known after apply)
      + public_dns                   = (known after apply)
      + public_ip                    = (known after apply)
      + security_groups              = (known after apply)
      + source_dest_check            = true
      + subnet_id                    = (known after apply)
      + tenancy                      = (known after apply)
      + volume_tags                  = (known after apply)
      + vpc_security_group_ids       = (known after apply)

      + ebs_block_device {
          + delete_on_termination = (known after apply)
          + device_name           = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + snapshot_id           = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (known after apply)
        }

      + ephemeral_block_device {
          + device_name  = (known after apply)
          + no_device    = (known after apply)
          + virtual_name = (known after apply)
        }

      + network_interface {
          + delete_on_termination = (known after apply)
          + device_index          = (known after apply)
          + network_interface_id  = (known after apply)
        }

      + root_block_device {
          + delete_on_termination = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (known after apply)
        }
    }

  # aws_instance.OneServer[1] will be created
  + resource "aws_instance" "OneServer" {
      + ami                          = "ami-00035f41c82244dab"
      + arn                          = (known after apply)
      + associate_public_ip_address  = (known after apply)
      + availability_zone            = (known after apply)
      + cpu_core_count               = (known after apply)
      + cpu_threads_per_core         = (known after apply)
      + get_password_data            = false
      + host_id                      = (known after apply)
      + id                           = (known after apply)
      + instance_state               = (known after apply)
      + instance_type                = "t2.micro"
      + ipv6_address_count           = (known after apply)
      + ipv6_addresses               = (known after apply)
      + key_name                     = "AWS-Cisco"
      + network_interface_id         = (known after apply)
      + password_data                = (known after apply)
      + placement_group              = (known after apply)
      + primary_network_interface_id = (known after apply)
      + private_dns                  = (known after apply)
      + private_ip                   = (known after apply)
      + public_dns                   = (known after apply)
      + public_ip                    = (known after apply)
      + security_groups              = (known after apply)
      + source_dest_check            = true
      + subnet_id                    = (known after apply)
      + tenancy                      = (known after apply)
      + volume_tags                  = (known after apply)
      + vpc_security_group_ids       = (known after apply)

      + ebs_block_device {
          + delete_on_termination = (known after apply)
          + device_name           = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + snapshot_id           = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (known after apply)
        }

      + ephemeral_block_device {
          + device_name  = (known after apply)
          + no_device    = (known after apply)
          + virtual_name = (known after apply)
        }

      + network_interface {
          + delete_on_termination = (known after apply)
          + device_index          = (known after apply)
          + network_interface_id  = (known after apply)
        }

      + root_block_device {
          + delete_on_termination = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (known after apply)
        }
    }

Plan: 2 to add, 0 to change, 0 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

And finally, issue the ‘terraform apply’ command.

WAUTERW-M-65P7:_Create_2_EC2_instance_3_files_remotestate wauterw$ terraform apply

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:

  # aws_instance.OneServer[0] will be created
  + resource "aws_instance" "OneServer" {
      + ami                          = "ami-00035f41c82244dab"
      + arn                          = (known after apply)
      + associate_public_ip_address  = (known after apply)
      + availability_zone            = (known after apply)
      + cpu_core_count               = (known after apply)
      + cpu_threads_per_core         = (known after apply)
      + get_password_data            = false
      + host_id                      = (known after apply)
      + id                           = (known after apply)
      + instance_state               = (known after apply)
      + instance_type                = "t2.micro"
      + ipv6_address_count           = (known after apply)
      + ipv6_addresses               = (known after apply)
      + key_name                     = "AWS-Cisco"
      + network_interface_id         = (known after apply)
      + password_data                = (known after apply)
      + placement_group              = (known after apply)
      + primary_network_interface_id = (known after apply)
      + private_dns                  = (known after apply)
      + private_ip                   = (known after apply)
      + public_dns                   = (known after apply)
      + public_ip                    = (known after apply)
      + security_groups              = (known after apply)
      + source_dest_check            = true
      + subnet_id                    = (known after apply)
      + tenancy                      = (known after apply)
      + volume_tags                  = (known after apply)
      + vpc_security_group_ids       = (known after apply)

      + ebs_block_device {
          + delete_on_termination = (known after apply)
          + device_name           = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + snapshot_id           = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (known after apply)
        }

      + ephemeral_block_device {
          + device_name  = (known after apply)
          + no_device    = (known after apply)
          + virtual_name = (known after apply)
        }

      + network_interface {
          + delete_on_termination = (known after apply)
          + device_index          = (known after apply)
          + network_interface_id  = (known after apply)
        }

      + root_block_device {
          + delete_on_termination = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (known after apply)
        }
    }

  # aws_instance.OneServer[1] will be created
  + resource "aws_instance" "OneServer" {
      + ami                          = "ami-00035f41c82244dab"
      + arn                          = (known after apply)
      + associate_public_ip_address  = (known after apply)
      + availability_zone            = (known after apply)
      + cpu_core_count               = (known after apply)
      + cpu_threads_per_core         = (known after apply)
      + get_password_data            = false
      + host_id                      = (known after apply)
      + id                           = (known after apply)
      + instance_state               = (known after apply)
      + instance_type                = "t2.micro"
      + ipv6_address_count           = (known after apply)
      + ipv6_addresses               = (known after apply)
      + key_name                     = "AWS-Cisco"
      + network_interface_id         = (known after apply)
      + password_data                = (known after apply)
      + placement_group              = (known after apply)
      + primary_network_interface_id = (known after apply)
      + private_dns                  = (known after apply)
      + private_ip                   = (known after apply)
      + public_dns                   = (known after apply)
      + public_ip                    = (known after apply)
      + security_groups              = (known after apply)
      + source_dest_check            = true
      + subnet_id                    = (known after apply)
      + tenancy                      = (known after apply)
      + volume_tags                  = (known after apply)
      + vpc_security_group_ids       = (known after apply)

      + ebs_block_device {
          + delete_on_termination = (known after apply)
          + device_name           = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + snapshot_id           = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (known after apply)
        }

      + ephemeral_block_device {
          + device_name  = (known after apply)
          + no_device    = (known after apply)
          + virtual_name = (known after apply)
        }

      + network_interface {
          + delete_on_termination = (known after apply)
          + device_index          = (known after apply)
          + network_interface_id  = (known after apply)
        }

      + root_block_device {
          + delete_on_termination = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (known after apply)
        }
    }

Plan: 2 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

aws_instance.OneServer[0]: Creating...
aws_instance.OneServer[1]: Creating...
aws_instance.OneServer[1]: Still creating... [10s elapsed]
aws_instance.OneServer[0]: Still creating... [10s elapsed]
aws_instance.OneServer[1]: Still creating... [20s elapsed]
aws_instance.OneServer[0]: Still creating... [20s elapsed]
aws_instance.OneServer[1]: Creation complete after 27s [id=i-0db5e337e49e61ec3]
aws_instance.OneServer[0]: Still creating... [30s elapsed]
aws_instance.OneServer[0]: Creation complete after 40s [id=i-034690f04e747ae29]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

instance_ip_addr = [
  "172.31.44.147",
  "172.31.32.28",
]
instance_ips = [
  "52.212.239.23",
  "34.249.211.127",
]

And obviously, you get back the IP addresses for the two instances. If all goes well, you will see two instances in AWS.

If you are following along, you will sure have noticed that this time there is no tfstate file in your local folder. Consult your configured S3 bucket and you will see that it is stored over there (as expected).

Thanks all for taking the time to read this post.

Configure AWS instances with Terraform: introduction

Intoduction

In this blogpost, we will create an AWS instance onto EC2 entirely using Terraform. Nothing too complex as such, but better to start with something fairly easy and continue to expand on it. Which is what we will do in next blog posts.

Code

We will start off with creating a file called ‘provider.tf’. As the filename suggests we will configure the Terraform AWS provider in that file.

provider "aws" {
  version    = "~> 2.0"
  access_key = "${var.ec2_aws_access_key}"
  secret_key = "${var.ec2_aws_secret_key}"
  region     = "${var.ec2_region}"
}

The meat of configuring EC2 instances is in the below file. I have the tendency to call this file the main.tf file but as you likely know, the filename is entirely up to your imagination.

As we want to create an EC2 instance, we will add a resource ‘aws_instance’ and provide it the arguments it requires. I think these speak for itself. In case you are interested what other arguments can be specified, please refer to the Terraform documentation.

As we want to know which IP addresses AWS assigned to our EC2 instance, we are also outputting some these. Again, if you are interested in all the arguments that can be output, please see here.

resource "aws_instance" "OneServer" {
  ami           = "${var.ec2_image}"
  instance_type = "${var.ec2_instance_type}"
  key_name      = "${var.ec2_keypair}"
  tags = {
    Name = "${var.ec2_tags}"
  }
}

output "instance_ip_addr" {
  value       = "${aws_instance.OneServer.private_ip}"
  description = "The private IP address of the main server instance."
}

output "instance_ips" {
  value = ["${aws_instance.OneServer.*.public_ip}"]
}

output "instance_id" {
  value = ["${aws_instance.OneServer.id}"]
}

You will notice that we are referencing some variables in that file so we also need to declare them. This is something we will do in the variables.tf file.

variable "ec2_aws_access_key" {
  default = "AKI***BZ5A"
}

variable "ec2_aws_secret_key" {
  default = "GDl***CnS7V"
}

variable "ec2_region" {
  default = "eu-west-1"
}

variable "ec2_image" {
  default = "ami-00035f41c82244dab"
}

variable "ec2_instance_type" {
  default = "t2.micro"
}

variable "ec2_keypair" {
  default = "AWS-Cisco"
}
variable "ec2_tags" {
  default = "Cisco-Demo-Terraform-1"
}

Deployment

The beauty of Terraform is its simplicity. Start off with running ‘terraform init’. This will essentially download the required plugins.

WAUTERW-M-T3ZT:Terraform_AWS_initial wim$ terraform init

Initializing the backend...

Initializing provider plugins...
- Checking for available provider plugins...
- Downloading plugin for provider "aws" (terraform-providers/aws) 2.12.0...

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.

Terraform plan will show you a blueprint of things that are about the created/updated. No surprise here we will see that the plan is to create 1 instance (see bottom of the output).

WAUTERW-M-65P7:_Create_1_EC2_instance_3_files wauterw$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


------------------------------------------------------------------------

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:

  # aws_instance.OneServer will be created
  + resource "aws_instance" "OneServer" {
      + ami                          = "ami-00035f41c82244dab"
      + arn                          = (known after apply)
      + associate_public_ip_address  = (known after apply)
      + availability_zone            = (known after apply)
      + cpu_core_count               = (known after apply)
      + cpu_threads_per_core         = (known after apply)
      + get_password_data            = false
      + host_id                      = (known after apply)
      + id                           = (known after apply)
      + instance_state               = (known after apply)
      + instance_type                = "t2.micro"
      + ipv6_address_count           = (known after apply)
      + ipv6_addresses               = (known after apply)
      + key_name                     = "AWS-Cisco"
      + network_interface_id         = (known after apply)
      + password_data                = (known after apply)
      + placement_group              = (known after apply)
      + primary_network_interface_id = (known after apply)
      + private_dns                  = (known after apply)
      + private_ip                   = (known after apply)
      + public_dns                   = (known after apply)
      + public_ip                    = (known after apply)
      + security_groups              = (known after apply)
      + source_dest_check            = true
      + subnet_id                    = (known after apply)
      + tags                         = {
          + "Name" = "Cisco-Demo-Terraform-1"
        }
      + tenancy                      = (known after apply)
      + volume_tags                  = (known after apply)
      + vpc_security_group_ids       = (known after apply)

      + ebs_block_device {
          + delete_on_termination = (known after apply)
          + device_name           = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + snapshot_id           = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (known after apply)
        }

      + ephemeral_block_device {
          + device_name  = (known after apply)
          + no_device    = (known after apply)
          + virtual_name = (known after apply)
        }

      + network_interface {
          + delete_on_termination = (known after apply)
          + device_index          = (known after apply)
          + network_interface_id  = (known after apply)
        }

      + root_block_device {
          + delete_on_termination = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (known after apply)
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

And finally, we will run ‘terraform’ apply. This will take the execution plan and effectively apply that plan to AWS.

WAUTERW-M-65P7:_Create_1_EC2_instance_3_files wauterw$ terraform apply

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:

  # aws_instance.OneServer will be created
  + resource "aws_instance" "OneServer" {
      + ami                          = "ami-00035f41c82244dab"
      + arn                          = (known after apply)
      + associate_public_ip_address  = (known after apply)
      + availability_zone            = (known after apply)
      + cpu_core_count               = (known after apply)
      + cpu_threads_per_core         = (known after apply)
      + get_password_data            = false
      + host_id                      = (known after apply)
      + id                           = (known after apply)
      + instance_state               = (known after apply)
      + instance_type                = "t2.micro"
      + ipv6_address_count           = (known after apply)
      + ipv6_addresses               = (known after apply)
      + key_name                     = "AWS-Cisco"
      + network_interface_id         = (known after apply)
      + password_data                = (known after apply)
      + placement_group              = (known after apply)
      + primary_network_interface_id = (known after apply)
      + private_dns                  = (known after apply)
      + private_ip                   = (known after apply)
      + public_dns                   = (known after apply)
      + public_ip                    = (known after apply)
      + security_groups              = (known after apply)
      + source_dest_check            = true
      + subnet_id                    = (known after apply)
      + tags                         = {
          + "Name" = "Cisco-Demo-Terraform-1"
        }
      + tenancy                      = (known after apply)
      + volume_tags                  = (known after apply)
      + vpc_security_group_ids       = (known after apply)

      + ebs_block_device {
          + delete_on_termination = (known after apply)
          + device_name           = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + snapshot_id           = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (known after apply)
        }

      + ephemeral_block_device {
          + device_name  = (known after apply)
          + no_device    = (known after apply)
          + virtual_name = (known after apply)
        }

      + network_interface {
          + delete_on_termination = (known after apply)
          + device_index          = (known after apply)
          + network_interface_id  = (known after apply)
        }

      + root_block_device {
          + delete_on_termination = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (known after apply)
        }
    }

Plan: 1 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

aws_instance.OneServer: Creating...
aws_instance.OneServer: Still creating... [10s elapsed]
aws_instance.OneServer: Still creating... [20s elapsed]
aws_instance.OneServer: Still creating... [30s elapsed]
aws_instance.OneServer: Creation complete after 40s [id=i-01f0430652eb0430b]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

instance_id = [
  "i-01f0430652eb0430b",
]
instance_ip_addr = 172.31.3.67
instance_ips = [
  [
    "34.254.199.197",
  ],
]

If you are following along with this blogpost, log in to your AWS account and you will see your instance up and running.

Something noteworthy is the tfstate that Terraform maintains. The tfstate is found in your local directory (from where you run the Terraform commands) and should never be touched. It contains all the information related to the state of your configuration. I added a full output below for you to verify.

{
  "version": 4,
  "terraform_version": "0.12.10",
  "serial": 2,
  "lineage": "9ce053fb-793f-6e61-efd7-bbc4ca54a7d9",
  "outputs": {
    "instance_id": {
      "value": [
        "i-01f0430652eb0430b"
      ],
      "type": [
        "tuple",
        [
          "string"
        ]
      ]
    },
    "instance_ip_addr": {
      "value": "172.31.3.67",
      "type": "string"
    },
    "instance_ips": {
      "value": [
        [
          "34.254.199.197"
        ]
      ],
      "type": [
        "tuple",
        [
          [
            "tuple",
            [
              "string"
            ]
          ]
        ]
      ]
    }
  },
  "resources": [
    {
      "mode": "managed",
      "type": "aws_instance",
      "name": "OneServer",
      "provider": "provider.aws",
      "instances": [
        {
          "schema_version": 1,
          "attributes": {
            "ami": "ami-00035f41c82244dab",
            "arn": "arn:aws:ec2:eu-west-1:852350637351:instance/i-01f0430652eb0430b",
            "associate_public_ip_address": true,
            "availability_zone": "eu-west-1b",
            "cpu_core_count": 1,
            "cpu_threads_per_core": 1,
            "credit_specification": [
              {
                "cpu_credits": "standard"
              }
            ],
            "disable_api_termination": false,
            "ebs_block_device": [],
            "ebs_optimized": false,
            "ephemeral_block_device": [],
            "get_password_data": false,
            "host_id": null,
            "iam_instance_profile": "",
            "id": "i-01f0430652eb0430b",
            "instance_initiated_shutdown_behavior": null,
            "instance_state": "running",
            "instance_type": "t2.micro",
            "ipv6_address_count": 0,
            "ipv6_addresses": [],
            "key_name": "AWS-Cisco",
            "monitoring": false,
            "network_interface": [],
            "network_interface_id": null,
            "password_data": "",
            "placement_group": "",
            "primary_network_interface_id": "eni-0e85df79e36b49d3a",
            "private_dns": "ip-172-31-3-67.eu-west-1.compute.internal",
            "private_ip": "172.31.3.67",
            "public_dns": "ec2-34-254-199-197.eu-west-1.compute.amazonaws.com",
            "public_ip": "34.254.199.197",
            "root_block_device": [
              {
                "delete_on_termination": true,
                "encrypted": false,
                "iops": 100,
                "kms_key_id": "",
                "volume_id": "vol-0bb5a836e1fe3f40d",
                "volume_size": 8,
                "volume_type": "gp2"
              }
            ],
            "security_groups": [
              "default"
            ],
            "source_dest_check": true,
            "subnet_id": "subnet-62df4504",
            "tags": {
              "Name": "Cisco-Demo-Terraform-1"
            },
            "tenancy": "default",
            "timeouts": null,
            "user_data": null,
            "user_data_base64": null,
            "volume_tags": {},
            "vpc_security_group_ids": [
              "sg-c3b88cbd"
            ]
          },
          "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMCwidXBkYXRlIjo2MDAwMDAwMDAwMDB9LCJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
        }
      ]
    }
  ]
}

Also deleting the instance is very simple using Terraform. Just issue a ‘terraform destroy’ command, hit enter, accept the change and off you go. No more instances on AWS. While easy and nice, I’m sure you will also understand the danger of this.

WAUTERW-M-65P7:_Create_1_EC2_instance_3_files wauterw$ terraform destroy
aws_instance.OneServer: Refreshing state... [id=i-01f0430652eb0430b]

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:

  # aws_instance.OneServer will be destroyed
  - resource "aws_instance" "OneServer" {
      - ami                          = "ami-00035f41c82244dab" -> null
      - arn                          = "arn:aws:ec2:eu-west-1:852350637351:instance/i-01f0430652eb0430b" -> null
      - associate_public_ip_address  = true -> null
      - availability_zone            = "eu-west-1b" -> null
      - cpu_core_count               = 1 -> null
      - cpu_threads_per_core         = 1 -> null
      - disable_api_termination      = false -> null
      - ebs_optimized                = false -> null
      - get_password_data            = false -> null
      - id                           = "i-01f0430652eb0430b" -> null
      - instance_state               = "running" -> null
      - instance_type                = "t2.micro" -> null
      - ipv6_address_count           = 0 -> null
      - ipv6_addresses               = [] -> null
      - key_name                     = "AWS-Cisco" -> null
      - monitoring                   = false -> null
      - primary_network_interface_id = "eni-0e85df79e36b49d3a" -> null
      - private_dns                  = "ip-172-31-3-67.eu-west-1.compute.internal" -> null
      - private_ip                   = "172.31.3.67" -> null
      - public_dns                   = "ec2-34-254-199-197.eu-west-1.compute.amazonaws.com" -> null
      - public_ip                    = "34.254.199.197" -> null
      - security_groups              = [
          - "default",
        ] -> null
      - source_dest_check            = true -> null
      - subnet_id                    = "subnet-62df4504" -> null
      - tags                         = {
          - "Name" = "Cisco-Demo-Terraform-1"
        } -> null
      - tenancy                      = "default" -> null
      - volume_tags                  = {} -> null
      - vpc_security_group_ids       = [
          - "sg-c3b88cbd",
        ] -> null

      - credit_specification {
          - cpu_credits = "standard" -> null
        }

      - root_block_device {
          - delete_on_termination = true -> null
          - encrypted             = false -> null
          - iops                  = 100 -> null
          - volume_id             = "vol-0bb5a836e1fe3f40d" -> null
          - volume_size           = 8 -> null
          - volume_type           = "gp2" -> null
        }
    }

Plan: 0 to add, 0 to change, 1 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

aws_instance.OneServer: Destroying... [id=i-01f0430652eb0430b]
aws_instance.OneServer: Still destroying... [id=i-01f0430652eb0430b, 10s elapsed]
aws_instance.OneServer: Still destroying... [id=i-01f0430652eb0430b, 20s elapsed]
aws_instance.OneServer: Still destroying... [id=i-01f0430652eb0430b, 30s elapsed]
aws_instance.OneServer: Destruction complete after 31s

Destroy complete! Resources: 1 destroyed.

In AWS, you will see the instance we created earlier in the blog post is in a terminated state.

Thanks for reading guys, hope to see you back in a next blog post some time.