DEV Community

Jon Lunsford
Jon Lunsford

Posted on

Deploying Elixir (2 of 3): Terraforming an AWS EC2 Instance

This post is the second in a series of three on deploying elixir:

  1. Building Releases with Docker & Mix
  2. Terraforming an AWS EC2 Instance
  3. Deploying Releases with Ansible

In Pt. 1 we built our production release, locally, with Docker. Now that we have a release, we need somewhere to run it. Let's walk through the basics of Terraform, by HashiCorp. By using Terraform, we can write our infrastructure as code, albeit a very simple infrastructure for our webhook processor app.

If you did not follow along with that last post, you can grab the complete code here: https://github.com/jonlunsford/webhook_processor


Terraform is:

A workflow to provision infrastructure for private cloud, public cloud, and external services. Build reusable Terraform templates to define the topology of infrastructure using code.

The goals for this post are:

  1. Install Terraform
  2. Configure Terraform for AWS
  3. Create an EC2 instance

Installing Terraform

If you're using homebrew on OSX, you can run:

$ brew install terraform
Enter fullscreen mode Exit fullscreen mode

Otherwise, you can grab a binary for your system here. Verify the install worked properly by opening a new shell and typing:

$ terraform
Enter fullscreen mode Exit fullscreen mode

You should see some usage output. If you see an error, it's possible the binary is not in your $PATH, ensure that the directory Terraform was installed in, is part of your $PATH.


Configuring Terraform for AWS

With Terraform installed, we can begin building infrastructure on AWS. You'll need an AWS account, grab a free one here if you don't already have one. Next you'll need valid credentials, which can be found in the IAM Management Console.

Finally, the last prerequisite is having an ssh key pair ready to supply, so we can eventually connect to our instance, both manually and through Ansible, outlined in the next post. Let's generate a new key to be used with our instance:

ssh-keygen -f ./rel/webhook_processor_key
Enter fullscreen mode Exit fullscreen mode

Configuration is composed in *.tf files, this is how we will describe the infrastructure we want. The following is the entire config file to build an EC2 instance with a new key pair and security group allowing http+ssh access, save this file at ./rel/terraform/webhook_processor.tf:

# ./rel/terraform/webhook_processor.tf

provider "aws" {
  region = "us-west-1" # Set the region available to you
  access_key = ACCESS_KEY_HERE
  secret_key = SECRET_KEY_HERE
}

# A security group is required to configure allowed traffic on the instance
resource "aws_security_group" "webhook_processor_sg" {
  name          = "allow-all-sg"
  description   = "Allow all inbound ssh+http traffic"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [ "0.0.0.0/0" ]
  }

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = [ "0.0.0.0/0" ]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# A key pair is required to ultimately allow SSH access
resource "aws_key_pair" "webhook_processor_kp" {
  key_name   = "webhook_processor_deploy"
  public_key = file("../webhook_processor_key.pub") # Read from the file we generated
}

# Finally, create the ec2 instance using the above references
resource "aws_instance" "webhook_processor" {
  ami             = "ami-0e4ae7403dc481431" # Debian Buster AMI
  instance_type   = "t2.micro" # Should be eligible for free
  key_name        = aws_key_pair.webhook_processor_kp.key_name
  security_groups = [aws_security_group.webhook_processor_sg.name]
}

output "webhook_processor_host" {
  value = aws_instance.webhook_processor.public_dns
}
Enter fullscreen mode Exit fullscreen mode

Replace ACCESS_KEY_HERE and SECRET_KEY_HERE with your own. You can hard code them this way for now or place your credentials in ~/.aws/credentials, that file should look like:

[default]
aws_access_key_id = ACCESS_KEY_HERE
aws_secret_access_key = SECRET_KEY_HERE
Enter fullscreen mode Exit fullscreen mode

If the access_key and secret_key are omitted from the provider block, Terraform will automatically look for the above file, in which case it will look like:

provider "aws" {
  region = "us-west-1"
}
Enter fullscreen mode Exit fullscreen mode

NOTE: If you use a region other than us-west-1 the AMI for Debian Buster will be different, AMI's are region specific. There are a few ways to find an AMI on AWS. Once you've found an AMI, be sure to replace the example above.


Let's go over the .tf file really quick. provider blocks are used to configure named providers, aws in our case. There are a whole bunch of providers available. Terraform files can have many providers configured as well, you can begin to see how you can describe very complex infrastructure topologies.

The resource block is used to describe a resource that our architecture requires, in this case we will need an aws_security_group, aws_key_pair and an aws_instance.

Finally, output blocks allow you to retrieve specific attributes of your resources. We will need to know the public_dns (url) of our instance when we deploy our app.


Initializing Terraform

Now we are ready to confirm our .tf file is correct. cd into ./rel/terraform/ and run the following:

$ terraform init
Enter fullscreen mode Exit fullscreen mode

This will prepare Terraform, including downloading any provider plugins it needs, you should see it downloading the provider.aws plugin.


Creating Infrastructure

After successful initialization we can now create our infrastructure, from the same directory (./rel/terraform/) run:

$ terraform apply
Enter fullscreen mode Exit fullscreen mode

You will see an overview of the execution plan along with a prompt to confirm applying the plan, something like:

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

 ...
Enter fullscreen mode Exit fullscreen mode

Notice + create, this indicates the type of action Terraform will perform. Since our infrastructure does not exist, it assumes we are creating new resources.


Changing Infrastructure

To get a little more comfortable with Terraform, let's change something and see how that is handled. Update the ami for our aws_instance:

resource "aws_instance" "webhook_processor" {
  ami             = "ami-29918949" # Different resource
  instance_type   = "t2.micro" # Should be eligible for free
  key_name        = aws_key_pair.webhook_processor_kp.key_name
  security_groups = [aws_security_group.webhook_processor_sg.name]
}
Enter fullscreen mode Exit fullscreen mode

Again, run:

$ terraform apply
Enter fullscreen mode Exit fullscreen mode

And again, we see an overview of the execution plan with a prompt to confirm:

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

...
Enter fullscreen mode Exit fullscreen mode

Notice -/+ destroy and then create replacement, changing the actual instance ami requires destroying the existing one and creating a new one.

Enter yes at the prompt to execute the plan.


Destroying Infrastructure

Lastly, let's see how Terraform can destroy resources it manages, run the following:

$ terraform destroy
Enter fullscreen mode Exit fullscreen mode

Here's the overview of that command:

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  - destroy

...
Enter fullscreen mode Exit fullscreen mode

We've now gone through the general workflow with Terraform. Go ahead and recreate an EC2 instance, so we have something to work with in part 3. Finally, let's confirm our ssh access is permitted, after you recreate the ec2 instance, test it out using the webhook_processor_host value that was printed out after, mine looks like:

ssh admin@ec2-xx-xxx-x-xxx.us-west-1.compute.amazonaws.com -i ../webhook_processor_key "cat ~/.ssh/authorized_keys"
Enter fullscreen mode Exit fullscreen mode

You should see the webhook_processor_deploy key printed out.


Conclusion

We've seen how to describe our infrastructure topology with Terraform and how to apply that to our providers, AWS in this case. You could just as easily apply this to other providers, DigitalOcean for example.

Up next, we will provision our newly created EC2 instance with Ansible and deploy our app.

As always, the code is available on GitHub: https://github.com/jonlunsford/webhook_processor

Top comments (2)

Collapse
 
mazz profile image
Michael

Hi thanks for putting this together.

I'm encountering an error at 'terraform apply'. I can't seem to make a security group.

This is my aws block

provider "aws" {
  region = "us-west-1" # Set the region available to you
  shared_credentials_file = "~/.aws/credentials"
  profile = "default"
}

and my ~/.aws/credentials file:

[default]
aws_access_key_id = secret
aws_secret_access_key = secret

any suggestions? I just generated the keys so I'm pretty sure they should work.


aws_key_pair.webhook_processor_kp: Creating...
aws_security_group.webhook_processor_sg: Creating...

Error: Error creating Security Group: UnauthorizedOperation: You are not authorized to perform this operation. Encoded authorization failure message: qCmU-SF-TxAb-pYfkSH6sR-Vn0CVDLDanDiiv4L9RTbOU9pU8DUH41XxSM5ZY0xsaX1-t-DUFAhzmZdJ7_ssFmRmyf_9-ZuLl9x90Ny-AeO2oErwyb8vn2ncvH80OwsCPfAFVhnURjdP-ScEXPPOGQSBl9HN4VDFvZIjE8aMN24y-lNGer3UlsHuMSmAzPp-NVLl4ZaAv0Wb6Ut3vaq0yloBAmBQhqQ3RAd5hbfD6NE4SA-CNQ4u1_2U7NUCdXJW4wLwWVlUljWnXcBGSpZF-P8LmDL1qTvmlzB3FrdbKgGhevb7E6dweNBA4LGynez-cktWtD31iDh0nIIVy7tW-ImjxWqZoAgx1EgKrm-Yk6FXeIrNLIbrOpxujKnbDo_ONASgQxZNFViWQU9ZwvF2A6OUkaEUCT0C2LNOVXlSRA69ccmDY-tA7onnbIujFbvjVxV-e8MdfcUEEAh9D75m3X837gvwL4USfl-jCpuau9r48bz0eXtBart685EWn5Su-JtNQB8H20qwnSU7aHZpYjPXSr8_N5o
    status code: 403, request id: cf69bd63-9855-4f1f-a192-414d520c3864

  on webhook_processor.tf line 10, in resource "aws_security_group" "webhook_processor_sg":
  10: resource "aws_security_group" "webhook_processor_sg" {



Error: Error import KeyPair: UnauthorizedOperation: You are not authorized to perform this operation. Encoded authorization failure message: _aR8ZUoQxse8lt9c3DBn6SwXgteZD32KMigZ3YN3ZJ0qhYzZASdRyv1_bN0HAJwvdQwVCKVae3ImWEVoI6l0G8APA4zhGdxzOeaeihPwvqddE9b1Qp_O06t2e62YgAAhPTJp5rA8Vnn1t3zsK-N2Lf--AVb5QwmFiAvzusjsAtTS0P-xfDUHwfhV_7qW8aCONfnYl7mjhyx98V7-tz9PmBJCfJ-gJKBI6eFwDcuep7rc30Pl8p7h8shU0jSZGvH8f5Txoz_oYGOorIRFSDTeLh-H2nqDekcXtqFBqD4PzcsZ_OuT4fc6c-K-s2uMGDEGr5RZGQEfQAEtF2Y2F_rGV80MnLDDLdjqtfoNb6_LB6riYu0DrnTANu09vUBOfZFFqfT0QojqHbHvEKMDmh6qdJMc-zvtUHtjaJbWcp9uNMoOQUqcaQG8h72gX3DwhIqVzxERDUlBkhla9kOF0V5vsbKVzstfwBf7wXCApOgMeLib32MlpgDs4tbF6GnxQXay5AB9ewl7rGpXDFh0Ct72VsEDiaX_ZAk9cqwze1UnNhzPcg
    status code: 403, request id: 5a659db4-46fe-404b-b554-a35f3c24a665

  on webhook_processor.tf line 37, in resource "aws_key_pair" "webhook_processor_kp":
  37: resource "aws_key_pair" "webhook_processor_kp" {
Collapse
 
mazz profile image
Michael

Nevermind! The problem was that I was using an IAM key. When I used a Root Key it worked.. Leaving this here for others.