Monday 23 May 2022

Managing AWS EC2 using Terraform

 

For provisioning EC2 instance, we need to use aws_instance resource. 

provider.tf:

provider "aws" {
    region = "eu-west-1"
}

main.tf:

resource "aws_instance" "my-web-server" {
    ami = "ami-0123456789abcdef"
    instance_type = "t2.micro"
    tags = {
        Name = "my-web-server"
        Description = "My Nginx on Ubuntu server"
    }
    user_data = <<EOF
                #!/bin/bash
                sudo apt update
                sudo apt install nginx
                systemctl enable nginx
                systemctl start nginx
                EOF
}


ami and instance_type are mandatory and tags and user_data are optional attributes. 
 
Same AMIs have different IDs, depending on the region. If some AMI is not available in the chosen region, terraform apply will issue the following error:

Error: creating EC2 Instance: InvalidAMIID.NotFound: The image id '[ami-033b95fb8078dc481]' does not exist
│       status code: 400, request id: 1818bb3d-6455-411a-b3e5-8e2cbca60371


Note that we didn't specify any storage for our EC2 instance. It is done automatically. AWS EC2 Instance Root Device | My Public Notepad
 
---

We could have also used variables (usually defined in separate file e.g. variables.tf) and set attributes' values to them like:

variable "ami" {
    default = "ami-0123456789abcdef"
}

resource "aws_instance" "my-web-server" {
    ami = var.ami
    ...
}
---
 
Instead of embedding the list of commands in configuration file, we could have used file function output as the value for user_data attribute:

install-nginx.sh:

#!/bin/bash
sudo apt -y update
sudo apt -y install nginx
sudo systemctl start nginx

main.tf:

resource "aws_instance" "my-web-server" {
    ...
    user_data = file("./install-nginx.sh")
}

If we add user_data after EC2 instance has already been provisioned, the next terraform apply will destroy this EC2 instance, create a new one and then execute user_data on it.
---
 
Instead of using user_data and install required software each time EC2 AMI is provisioned, it might be more time- and resource-efficient to build a custom AMI which contains this software and then use this AMI. This way software is installed only once.
---

terraform apply will now provision this resource and we'll have our server up and running.

At this moment, we cannot access this machine (via SSH) as we don't know its IP address and we haven't specified a key pair.

We can reuse an existing key pair by provisioning aws_key_pair resource in main.tf:

resource "aws_key_pair" "my-webserver" {
    public_key = file(pathexpand("~/.ssh/my-webserver.pub"))
}

public_key is mandatory argument while key_name, key_name_prefix and tags are optional.

We assumed here that public key is present on the local machine running Terraform. (PKA key pair might have been created on this machine or copied from another machine but it is not yet present among AWS EC2 key pairs - AWS key pair resource is yet to be created!)

We could have also embedded the public key as a string:

resource "aws_key_pair" "my-webserver" {
    public_key = "ssh-rsa ABCD234234....Dcg464wf user@iac-server"
}

We can now refer to this key from aws_instance resource:

 resource "aws_instance" "my-web-server" {
    ...
    key_nameaws_key_pair.my-webserver.id
    ...
}

To provision networking required to access the EC2 instance we need to provision AWS Security Group resource, just like when provisioning EC2 instance manually. aws_security_group block is used for this:

resource "aws_security_group" "ssh-access" {
    name = "ssh-access"
    description = "Allows SSH connection from anywhere"
    ingress {
        from_port = 22
        to_port = 22
        protocol = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
    }
}

---
 
NOTE: There are two ways do define certain attributes (ingress rules in our case above):
  • attributes as blocks (block syntax) - where values for all options need to be provided
  • dynamic blocks (as above) - we only need to provide values for options of interest
 
---
 
We can now reference this security group from our EC2 instance resource:

 resource "aws_instance" "my-web-server" {
    ...
    vpc_security_group_ids = [ aws_security_group.ssh-access.id ]
    ...
}

For manual SSH connection we also want to know the public IP address that will get assigned to our EC2 instance once this one is provisioned. We can use the output variable to capture it:

output public_ip {
    value = aws_instance.my-web-server.public_ip
}

The value of this variable gets displayed in the output of terraform apply
 
Example terraform apply output snippet:
...
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Outputs:
public_ip = "35.145.57.118"
 
We can then SSH to this server manually:

$ ssh -i ~/.ssh/my-webserver.pem <user_name>@<public_IP>


For Amazon Linux 2 or the Amazon Linux AMI, the user name is ec2-user
For an Ubuntu AMI, the user name is ubuntu.
...
 

We can also add -v option to ssh command in order to enable debug mode:

$ ssh -i ~/.ssh/my-webserver.pem ec2-user@35.145.57.118 -v
 
 
We use the public IPv4 address to access this server. However, when this server is rebooted or recreated, this IP address would change. To fix this, we can create an Elastic IP Address by using aws_eip resource. It is a static IPv4 address which does not change over time.

resource "aws_eip" "my-web-server-eip" {
    instance = aws_instance.my-web-server.id
    vpc = true
}

---

Resources:


No comments: