In this section of our series, we will focus on how to get our environment ready and how we can create simple infrastructure with few lines of Terraform code.
Create your AWS IAM user with access key and secret
We will be using AWS for our infrastructure. Since it’s not a good idea to use the root account we will need to create an IAM user with policies like:
- EC2FullAccess
- RDSFullAccess
- S3FullAccess
This serverless stack article illustrates what we are hoping to achieve with our IAM permission, but instead of using the admin policy as illustrated in the article, rather dilute it a little with the 3 managed policies listed above.
Optionally install the AWS CLI
This step was important for my setup because I manage multiple AWS accounts and leverage on aws cli profile . If this is your first time using aws-cli, you would soon enough find out how straightforward it is.
You will need to configure the cli after installation with the aws_access_key_id and aws_secret_access_key got from IAM creation above. In case you have the aws-cli already configured, it’s up to you to use a new profile or use your default IAM user etc. The command below shows how to get the cli installed through pip and how to configure it.
1 2 |
$pip install --user boto3 boto awscli $ aws configure |
Initialize Terraform
Let’s create our terraform project folder and the _main.tf file. Because terraform supports the creation of an infrastructure in multiple cloud providers it needs specific provider plugins in order to execute the instructions in a specific provider cloud platform. We will be using AWS so our plugin will be aws.
1 2 3 |
$ mkdir intro-to-terraform-aws $ cd intro-to-terraform-aws $ touch _main.ft |
Our _main.ft should similar to the snippet below
1 2 3 4 5 |
# _main.ft provider "aws" { region = "us-east-1" profile = "myprivatecloud". // This is using the IAM profile } |
Let’s initialize our terraform with the code below
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// This is from our $ terraform init // it produces the output below Initializing the backend... Initializing provider plugins... - Checking for available provider plugins... - Downloading plugin for provider "aws" (hashicorp/aws) 2.66.0... //This part is removed for sake of brevity * provider.aws: version = "~> 2.66" 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. //This part is removed for sake of brevity |
Proof of concept infrastructure
In this proof of concept, we will only launch an EC2 node with 2 lines of Terraform, but can’t really do much with the node though. In the code below we used an Amazon Linux AMI Id and the instance type/size t2.nano. The codes for this part of the tutorial is available at https://github.com/kdjomeda/intro-to-terraform/tree/terraform-poc
1 2 3 4 5 |
#intro-to-terraform-aws/main.tf resource "aws_instance" "proof-of-concept-node" { ami = "ami-09d95fab7fff3776c" instance_type = "t2.nano" } |
In the code above, we used the aws_instance resource of Terraform and defined an AMI (Amazon Linux AMI id) and the instance_type.
Though optional these days, we should run the Terraform plan to see the actions that will be performed. Now, at the risk of making this tutorial too long, I would like to explain the output of the Terraform Plan command
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
# from intro-to-terraform-aws folder $ 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.proof-of-concept-node will be created + resource "aws_instance" "proof-of-concept-node" { + ami = "ami-09d95fab7fff3776c" + 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.nano" + ipv6_address_count = (known after apply) + ipv6_addresses = (known after apply) + key_name = (known after apply) + network_interface_id = (known after apply) + outpost_arn = (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) } + metadata_options { + http_endpoint = (known after apply) + http_put_response_hop_limit = (known after apply) + http_tokens = (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) + device_name = (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. |
Though it can look meaningless there are a number of symbols you need to understand each of them, as some of them would mean that your resource will be replaced thus destroyed first then recreated. I think it’s really important to understand the implications of the actions. Above you have more or less the + sign which means most of the other resources will be created and in this case, using default values from AWS aside those we specified ourselves. The rest of the symbols are explained below:
- + create (will create the resource)
- – destroy (will destroy the resource )
- -/+ replace (will destroy and then create, or vice-versa if create-before-destroy is used)
- ~ update in-place (will update by adding or remove extra properties without creating or destroying)
- <= read (will read the resource, usually defined data resource)
To execute the plan shown for the terraform plan command, we need a Terraform Apply command which outputs information similar to the one of terraform plan. But this requires confirmation from its prompt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# $ terraform apply //This part, similar to one of terraform plan is removed for sake of brevity Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: //(type yes to start the execution) //this part is removed for sake of brevity //.... aws_instance.proof-of-concept-node: Still creating... [2m40s elapsed] aws_instance.proof-of-concept-node: Creation complete after 2m44s [id=i-00ef1fccdbac02959] Apply complete! Resources: 1 added, 0 changed, 0 destroyed. |
The command Terraform Destroy is what we need to remove everything we have set up using the apply command. Just like the Apply command the Destroy command requires a “yes” confirmation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
# from intro-to-terraform-aws folder $ terraform destroy aws_instance.proof-of-concept-node: Refreshing state... [id=i-00ef1fccdbac02959] 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.proof-of-concept-node will be destroyed //This part is removed for sake of brevity 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 //This part is removed for sake of brevity aws_instance.proof-of-concept-node: Still destroying... [id=i-00ef1fccdbac02959, 1m20s elapsed] aws_instance.proof-of-concept-node: Destruction complete after 1m22s Destroy complete! Resources: 1 destroyed. |
Creating a simple infra with no VPC
In this part of our tutorial, we will be creating the same kind of infrastructure but this time we will use a CentOS AMI and we will create our instance so that we can log into it using ssh, unlike the previous infra with which we can’t do anything with. The codes for this part of the tutorial is available at https://github.com/kdjomeda/intro-to-terraform/tree/single-instance-no-vpc
What do we really need to have a node only we can connect to?
- A public IP
- A Security Group to allow ssh connection
- A SSH key pair
Before we jump to the code, let’s create our own key pair. Of course we could use one already generated by AWS but I just want to use this opportunity to show how you could create and maintain our own that will be part of your automation .
1 2 |
# from any folder beside the terraform tutorial folder $ ssh-keygen -b 4096 -f MyIdentity.pem -m PEM -C "AWS Tutorial Usage" |
This will create the private key MyIdentity.pem and its public Key MyIdentity.pem.pub. We will use the public key in Terraform on the aws_key_pair resource. Your code should look similar to the one below. Please note that the public_key property of the aws_key_pair will have the content of the created MyIndentity.pem.pub
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
# main.tf resource "aws_security_group" "allow_ssh_from_public" { name = "single_node_secgroup" description = "simple Security Group for the single node" ingress { from_port = 22 // port to allow connection from ie to allow port range such as 20-26. this will be 20 to_port = 22 // port to allow connection to ie to allow port range such as 20-26. this will be 26 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] // This opens the node to the world. This is not good for production systems description = "allow connection to ssl port" } egress { // most of the time the egress is always needed. Here you can retrict the node out bound connection from_port = 0 protocol = "-1" to_port = 0 cidr_blocks = ["0.0.0.0/0"] } } resource "aws_key_pair" "my-identity-pem" { public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC8YsFK........+A0OVTiUn7ZcndCTw== AWS Tutorial Usage" //the content of MyIdentify.pem.pub goes here key_name = "MyIdentity.pem" } resource "aws_instance" "single-node-no-vpc" { ami = "ami-03248a0341eadb1f1" instance_type = "t2.nano" associate_public_ip_address = true // This allows the AWS to assign a public IP to the nonde security_groups = [aws_security_group.allow_ssh_from_public.name] // assigning the security group defined above key_name = aws_key_pair.my-identity-pem.key_name tags = { Name = "SingleNodeNoVPC" Env = "Tutorial" Product = "DemoPurpose" Terraform = true } } |
You can choose to execute the terraform plan before but here I encourage you to use the terraform apply to see the difference.
At the end of the execution, we could see the AWS assigned public IP to our box (We could see this from the Terraform output that we will talk about later). Let’s try and ssh into the box:
1 2 3 4 5 6 7 8 |
ssh -i MyIdentity.pem ec2-user@54.162.49.168 The authenticity of host '54.162.49.168 (54.162.49.168)' can't be established. ECDSA key fingerprint is SHA256:puKJt3eFnvVyGDWb/qwdrm6G24XyrjYOT4gCVqfGbOk. Are you sure you want to continue connecting (yes/no)? yes Warning: Permanently added '54.162.49.168' (ECDSA) to the list of known hosts. -bash: warning: setlocale: LC_CTYPE: cannot change locale (UTF-8): No such file or directory [ec2-user@ip-172-31-47-234 ~]$ cat /etc/centos-release CentOS Linux release 7.7.1908 (Core) |
1 2 |
# intro-to-terraform-aws $ terraform destroy |
In the part 2 of this tutorial, we will launch our EC2 instance in a VPC