{"id":486,"date":"2017-03-08T07:17:40","date_gmt":"2017-03-08T07:17:40","guid":{"rendered":"http:\/\/tuxlabs.com\/?p=486"},"modified":"2017-03-08T17:50:51","modified_gmt":"2017-03-08T17:50:51","slug":"how-to-launch-a-jump-host-in-aws-using-terraform","status":"publish","type":"post","link":"https:\/\/tuxlabs.com\/?p=486","title":{"rendered":"How To: Launch A Jump Host In AWS Using Terraform"},"content":{"rendered":"<p><a href=\"http:\/\/tuxlabs.com\/wp-content\/uploads\/2017\/03\/terraform-logo.png\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-548\" src=\"http:\/\/tuxlabs.com\/wp-content\/uploads\/2017\/03\/terraform-logo.png\" alt=\"\" width=\"1055\" height=\"514\" \/><\/a><\/p>\n<p>I have been a <a href=\"https:\/\/www.hashicorp.com\/\">Hashicorp<\/a> fan boy for a couple\u00a0of years now. I am impressed, and happy with pretty much everything they have done from Vagrant to Consul and more. In short they make the DevOps world a better place. That being said this article is about the aptly named <a href=\"https:\/\/www.terraform.io\/\"><strong>Terraform<\/strong><\/a> product. Here is how <a href=\"https:\/\/www.hashicorp.com\/\">Hashicorp<\/a> describes Terraform in their own words&#8230;<\/p>\n<p><em>&#8220;Terraform enables you to safely and predictably create, change, and improve production infrastructure. It is an open source tool that codifies APIs into declarative configuration files that can be shared amongst team members, treated as code, edited, reviewed, and versioned.&#8221;<\/em><\/p>\n<p>Interestingly, enough it doesn&#8217;t point out, but in a way implies it by omitting anything about providers that Terraform is multi-cloud (or cloud agnostic). Terraform works with<a href=\"https:\/\/www.terraform.io\/docs\/providers\/index.html\"> AWS, GCP, Azure and Openstack<\/a>. In this article we will be covering how to use Terraform with AWS.<\/p>\n<p><strong>Step 1, download Terraform, I am not going to cover that part \ud83d\ude09<\/strong><br \/>\n<a href=\"https:\/\/www.terraform.io\/downloads.html\">https:\/\/www.terraform.io\/downloads.html<\/a><\/p>\n<p><strong>Step 2, Configuration&#8230;<\/strong><\/p>\n<h3>Configuration<\/h3>\n<p>Hashicorp uses their own configuration language for Terraform, it is fully JSON compatible, which is nice.. The details are covered here <a href=\"https:\/\/github.com\/hashicorp\/hcl\">https:\/\/github.com\/hashicorp\/hcl<\/a>.<\/p>\n<p>After downloading and installing Terraform, its time to start generating the configs.<\/p>\n<h3>AWS IAM Keys<\/h3>\n<p>AWS keys are required to do anything with Terraform. You can read about how to generate an access key \/ secret key for a user here :\u00a0<a href=\"http:\/\/docs.aws.amazon.com\/IAM\/latest\/UserGuide\/id_credentials_access-keys.html#Using_CreateAccessKey\">http:\/\/docs.aws.amazon.com\/IAM\/latest\/UserGuide\/id_credentials_access-keys.html#Using_CreateAccessKey<\/a><\/p>\n<h3>Terraform Configuration Files Overview<\/h3>\n<p>When you execute the terraform commands, you should be within a directory containing terraform configuration files. Those files ending in a &#8216;.tf&#8217; extension will be loaded by Terraform in alphabetical order.<\/p>\n<p>Before we jump into our configuration files for our Jump box, it may be helpful to review a quick primer on the syntax here\u00a0<a href=\"https:\/\/www.terraform.io\/docs\/configuration\/syntax.html\">https:\/\/www.terraform.io\/docs\/configuration\/syntax.html<\/a><\/p>\n<p>&amp; some more advanced features such as lookup here\u00a0<a href=\"https:\/\/www.terraform.io\/docs\/configuration\/interpolation.html\">https:\/\/www.terraform.io\/docs\/configuration\/interpolation.html<\/a><\/p>\n<p>Most users of Terraform choose to make their configs modular resulting in multiple .tf files with names like main.tf, variables.tf and data.tf&#8230; This is not required&#8230;you can choose to put everything in one big Terraform file, but I caution you modularity\/compartmentalization is always a better approach than one monolithic file. Let&#8217;s take a look at our main.tf<\/p>\n<h3>Main.tf<\/h3>\n<p>Typically if you only see one terraform config file, it is called main.tf, most commonly there will be at least one other file called variables.tf used specifically for providing values for variables used in other TF files such as main.tf. Let&#8217;s take a look at our main.tf file section by section.<\/p>\n<h4>Provider<\/h4>\n<p>The provider keyword\u00a0is used to identify the platform (cloud) you will be talking to, whether it is AWS, or another cloud. In our case it is AWS, and define three configuration items, an access key, secret key, and a region all of which we are inserting variables for, which will later be looked up \/ translated into real values in variables.tf<\/p>\n<pre class=\"lang:default decode:true\">provider \"aws\" {\r\n        access_key = \"${var.aws_access_key_id}\"\r\n        secret_key = \"${var.aws_secret_access_key}\"\r\n        region = \"${var.aws_region}\"\r\n}<\/pre>\n<h4>Resource aws_instance<\/h4>\n<p>This section defines a resource, which is an &#8220;aws_instance&#8221; that we are calling &#8220;jump_box&#8221;. We define all configuration requirements for that instance. Substituting variable names where necessary, and in some cases we just hard code the value. Notice we\u00a0are attaching two security groups to the instance to allow for ICMP &amp; SSH. We are also tagging our instance, which is critical an AWS environment so your administrators\/teammates have some idea about the machine that is spun up and what it is used for.<\/p>\n<pre class=\"lang:default decode:true\">resource \"aws_instance\" \"jump_box\" {\r\n        ami = \"${lookup(var.ami_base, \"centos-7\")}\"\r\n        instance_type = \"t2.medium\"\r\n        key_name = \"${var.key_name}\"\r\n        vpc_security_group_ids = [\r\n                \"${lookup(var.security-groups, \"allow-icmp-from-home\")}\",\r\n                \"${lookup(var.security-groups, \"allow-ssh-from-home\")}\"\r\n        ]\r\n        subnet_id = \"${element(var.subnets_private, 0)}\"\r\n        root_block_device {\r\n                volume_size = 8\r\n                volume_type = \"standard\"\r\n        }\r\n        user_data = &lt;&lt;-EOF\r\n                                #!\/bin\/bash\r\n                                yum -y update\r\n                                EOF\r\n        tags = {\r\n                Name = \"${var.instance_name_prefix}-jump\"\r\n                ApplicationName = \"jump-box\"\r\n                ApplicationRole = \"ops\"\r\n                Cluster = \"${var.tags[\"Cluster\"]}\"\r\n                Environment = \"${var.tags[\"Environment\"]}\"\r\n                Project = \"${var.tags[\"Project\"]}\"\r\n                BusinessUnit = \"${var.tags[\"BusinessUnit\"]}\"\r\n                OwnerEmail = \"${var.tags[\"OwnerEmail\"]}\"\r\n                SupportEmail = \"${var.tags[\"SupportEmail\"]}\"\r\n        }\r\n}<\/pre>\n<p>Btw, the this resource type is provided by an AWS module found here\u00a0https:\/\/www.terraform.io\/docs\/providers\/aws\/r\/instance.html<\/p>\n<p>You have to download the module using terraform get (which can be done once you write some config files and type terraform get \ud83d\ude42 ).<\/p>\n<p>Also, note the usage of &#8216;user_data&#8217; here to update the machines packages at boot time. This is an <a href=\"http:\/\/docs.aws.amazon.com\/AWSEC2\/latest\/UserGuide\/user-data.html\">AWS feature <\/a>that is exposed through the AWS module in terraform.<\/p>\n<h4>Resource aws_security_group<\/h4>\n<p>Next we define a new security group (vs attaching an existing one in the above section). We are creating this new security group for other VM&#8217;s in the environment to attach later, such that it can be used to allow SSH from the Jump host to the VM&#8217;s in the environment.<\/p>\n<p>Also notice under cidr_blocks we define a single IP address a \/32 of our jump host&#8230;but more important is to notice how we determine that jump hosts IP address. Using .private_ip to access the attribute of the &#8220;jump_box&#8221; aws_instance we are creating\/just created in AWS. That is pretty cool.<\/p>\n<pre class=\"lang:default decode:true \">resource \"aws_security_group\" \"jump_box_sg\" {\r\n        name = \"${var.instance_name_prefix}-allow-ssh-from-jumphost\"\r\n        description = \"Allow SSH from the jump host\"\r\n        vpc_id = \"${var.vpc_id}\"\r\n\r\n        ingress {\r\n                from_port = 22\r\n                to_port = 22\r\n                protocol = \"tcp\"\r\n                cidr_blocks = [\"${aws_instance.jump_box.private_ip}\/32\"]\r\n        }\r\n\r\n        tags = \"${var.tags_infra_default}\"\r\n}<\/pre>\n<h4>Resource aws_route53_record<\/h4>\n<p>The last entry in our main.tf creates a DNS entry for our jump host in Route53. Again notice we are specifying a name of jump. has a prefix to an entry, but the remainder of the FQDN is figured out by the lookup command. \u00a0The lookup command is used to lookup values inside of a map. In this case the map is defined in our variables.tf that we will review next.<\/p>\n<pre class=\"lang:default decode:true\">resource \"aws_route53_record\" \"jump_box_dns\" {\r\n        zone_id = \"${lookup(var.route53_zone, \"id\")}\"\r\n        type = \"A\"\r\n        ttl = \"300\"\r\n        name = \"jump.${lookup(var.route53_zone, \"name\")}\"\r\n        records = [\"${aws_instance.jump_box.private_ip}\"]\r\n}<\/pre>\n<h3>Variables.tf<\/h3>\n<p>I will attempt to match the section structure I used above for main.tf when explaining the variables in variables.tf though it is not really in as clear of a layout using sections.<\/p>\n<h4>Provider variables<\/h4>\n<p>When terraform is run it compiles all .tf files, and replaces any key that equals a variable, with the value it finds listed in the variables.tf file (in our case) with the variable keyword\u00a0as a prefix. Notice that the first two variables are empty, they have no value defined. Why ? Terraform supports taking input at runtime, by leaving these values blank, Terraform will prompt us for the values. Region is pretty straight forward, default is the value returned and description in this case is really an unused value except as a comment.<\/p>\n<pre class=\"lang:default decode:true\">variable \"aws_access_key_id\" {}\r\nvariable \"aws_secret_access_key\" {}\r\n\r\nvariable \"aws_region\" {\r\n        description = \"AWS region to create resources in\"\r\n        default = \"us-east-1\"\r\n}<\/pre>\n<p>I would like to demonstrate the behavior of Terraform as described above, when the variables are left empty<\/p>\n<pre class=\"lang:default decode:true \">\u279c  jump terraform plan\r\nvar.aws_access_key_id\r\n  Enter a value: aaaa\r\n\r\nvar.aws_secret_access_key\r\n  Enter a value: bbbb\r\n\r\nRefreshing Terraform state in-memory prior to plan...\r\nThe refreshed state will be used to calculate this plan, but\r\nwill not be persisted to local or remote state storage.\r\n\r\n...<\/pre>\n<p>At this phase you would enter your AWS key info, and terraform would &#8216;plan&#8217; out your deployment. Meaning it would run through your configs, and print to your screen it&#8217;s plan, but not actually change any state in AWS. That is the difference between Terraform plan &amp; Terraform apply.<\/p>\n<h4>Resource aws_instance variables<\/h4>\n<p>Here we define the values of our AMI, SSH Key, Instance prefix for the name, Tags, security groups, and subnets. Again this should be pretty straight forward, no magic here, just the use of string variables &amp; maps where necessary.<\/p>\n<pre class=\"lang:default decode:true\">variable \"ami_base\" {\r\n        description = \"AWS AMIs for base images\"\r\n        default = {\r\n                \"centos-7\" = \"ami-2af1ca3d\"\r\n                \"ubuntu-14.04\" = \"ami-d79487c0\"\r\n        }\r\n}\r\n\r\nvariable \"key_name\" {\r\n        default = \"tuxninja-rsa-2048\"\r\n}\r\n\r\nvariable \"instance_name_prefix\" {\r\n        default = \"tuxlabs-\"\r\n}\r\n\r\nvariable \"tags\" {\r\n        type = \"map\"\r\n        default = {\r\n                        ApplicationName = \"Jump\"\r\n                        ApplicationRole = \"jump box - bastion\"\r\n                        Cluster = \"Jump\"\r\n                        Environment = \"Dev\"\r\n                        Project = \"Jump\"\r\n                        BusinessUnit = \"TuxLabs\"\r\n                        OwnerEmail = \"tuxninja@tuxlabs.com\"\r\n                        SupportEmail = \"tuxninja@tuxlabs.com\"\r\n        }\r\n}\r\n\r\nvariable \"tags_infra_default\" {\r\n        type = \"map\"\r\n        default = {\r\n                        ApplicationName = \"Jump\"\r\n                        ApplicationRole = \"jump box - bastion\"\r\n                        Cluster = \"Jump\"\r\n                        Environment = \"DEV\"\r\n                        Project = \"Jump\"\r\n                        BusinessUnit = \"TuxLabs\"\r\n                        OwnerEmail = \"tuxninja@tuxlabs.com\"\r\n                        SupportEmail = \"tuxninja@tuxlabs.com\"\r\n        }\r\n}\r\n\r\nvariable \"security-groups\" {\r\n        description = \"maintained security groups\"\r\n        default = {\r\n                \"allow-icmp-from-home\" = \"sg-a1b75ddc\"\r\n                \"allow-ssh-from-home\" = \"sg-aab75dd7\"\r\n        }\r\n}\r\n\r\nvariable \"vpc_id\" {\r\n        description = \"VPC us-east-1-vpc-tuxlabs-dev01\"\r\n        default = \"vpc-c229daa5\"\r\n}\r\n\r\nvariable \"subnets_private\" {\r\n        description = \"Private subnets within us-east-1-vpc-tuxlabs-dev01 vpc\"\r\n        default = [\"subnet-78dfb852\", \"subnet-a67322d0\", \"subnet-7aa1cd22\", \"subnet-75005c48\"]\r\n}\r\n\r\nvariable \"subnets_public\" {\r\n        description = \"Public subnets within us-east-1-vpc-tuxlabs-dev01 vpc\"\r\n        default = [\"subnet-7bdfb851\", \"subnet-a57322d3\", \"subnet-47a1cd1f\", \"subnet-73005c4e\"]\r\n}<\/pre>\n<p>It&#8217;s important to note the variables above are also used in other sections as needed, such as the aws_security_group section in main.tf &#8230;<\/p>\n<h4>Resource aws_route53_record variables<\/h4>\n<p>Here we define the ID &amp; Name that are used in the &#8216;lookup&#8217; functionality from our main.tf Route53 section above.<\/p>\n<pre class=\"lang:default decode:true\">variable \"route53_zone\" {\r\n        description = \"Route53 zone used for DNS records\"\r\n        default = {\r\n                id = \"Z1ME2RCUVBYEW2\"\r\n                name = \"tuxlabs.com\"\r\n        }\r\n}<\/pre>\n<p>It&#8217;s important to note Terraform or TF files do not care when or where things are loaded. All files are loaded and variables require no specific order consistent with any other part of the configuration. All that is required is that for each variable you try to insert a value for, it has a value listed via the variable keyword in a TF file somewhere.<\/p>\n<h3>Output.tf<\/h3>\n<p>Again, I want to remind folks you can put these terraform syntax in one file if you wanted to, but I choose to split things up for readability and simplicity. So we have an output.tf file specifically for the output command, there is only one command, which lists the results of our terraform configurations upon success.<\/p>\n<pre class=\"lang:default decode:true \">output \"jump-box-details\" {\r\n\tvalue = \"${aws_route53_record.jump_box_dns.fqdn} - ${aws_instance.jump_box.private_ip} - ${aws_instance.jump_box.id} - ${aws_instance.jump_box.availability_zone}\"\r\n}<\/pre>\n<p>Ok so let&#8217;s run this and see how it looks&#8230;First a reminder, to test your config you can run Terraform plan first..It will tell you the changes its going to make&#8230;example<\/p>\n<pre class=\"lang:default decode:true\">\u279c  jump terraform plan\r\nvar.aws_access_key_id\r\n  Enter a value: blahblah\r\n\r\nvar.aws_secret_access_key\r\n  Enter a value: blahblahblahblah\r\n\r\nRefreshing Terraform state in-memory prior to plan...\r\nThe refreshed state will be used to calculate this plan, but\r\nwill not be persisted to local or remote state storage.\r\n\r\n\r\nThe Terraform execution plan has been generated and is shown below.\r\nResources are shown in alphabetical order for quick scanning. Green resources\r\nwill be created (or destroyed and then created if an existing resource\r\nexists), yellow resources are being changed in-place, and red resources\r\nwill be destroyed. Cyan entries are data sources to be read.\r\n\r\nNote: You didn't specify an \"-out\" parameter to save this plan, so when\r\n\"apply\" is called, Terraform can't guarantee this is what will execute.\r\n\r\n+ aws_instance.jump_box\r\n    ami:                                       \"ami-2af1ca3d\"\r\n    associate_public_ip_address:               \"&lt;computed&gt;\"\r\n    availability_zone:                         \"&lt;computed&gt;\"\r\n    ebs_block_device.#:                        \"&lt;computed&gt;\"\r\n    ephemeral_block_device.#:                  \"&lt;computed&gt;\"\r\n    instance_state:                            \"&lt;computed&gt;\"\r\n    instance_type:                             \"t2.medium\"\r\n\r\n...\r\n\r\nPlan: 3 to add, 0 to change, 0 to destroy.<\/pre>\n<p>If everything looks good &amp; is green, you are ready to apply.<\/p>\n<pre class=\"lang:default decode:true \">aws_security_group.jump_box_sg: Creation complete\r\naws_route53_record.jump_box_dns: Still creating... (10s elapsed)\r\naws_route53_record.jump_box_dns: Still creating... (20s elapsed)\r\naws_route53_record.jump_box_dns: Still creating... (30s elapsed)\r\naws_route53_record.jump_box_dns: Still creating... (40s elapsed)\r\naws_route53_record.jump_box_dns: Still creating... (50s elapsed)\r\naws_route53_record.jump_box_dns: Creation complete\r\n\r\nApply complete! Resources: 3 added, 0 changed, 0 destroyed.\r\n\r\nThe state of your infrastructure has been saved to the path\r\nbelow. This state is required to modify and destroy your\r\ninfrastructure, so keep it safe. To inspect the complete state\r\nuse the `terraform show` command.\r\n\r\nState path: terraform.tfstate\r\n\r\nOutputs:\r\n\r\njump-box-details = jump.tuxlabs.com - 10.10.195.46 - i-037f5b15bce6cc16d - us-east-1b\r\n\u279c  jump<\/pre>\n<p>Congratulations, you now have a jump box in AWS using Terraform. Remember to attach the required security group to each machine you want to grant access to, and start locking down your jump box \/ bastion and VM&#8217;s.<\/p>\n<h3>Outro<\/h3>\n<p>Remember if you take the above config and try to run it, swapping out only the variables it will error something about a required module. Downloading the required modules is as simple as typing &#8216;<strong>terraform get<\/strong>&#8216; , which I believe the error message even tells you \ud83d\ude42<\/p>\n<p>So again this was a brief intro to Terraform it does a lot &amp; is extremely powerful. One of the thing I did when setting up a Mongo cluster using Terraform, was to take advantage of a map to change node count per region. So if you wanted to deploy a different number of instances in different regions, your config might look something like&#8230;<\/p>\n<h3>main.tf<\/h3>\n<pre class=\"lang:default decode:true\">  count = \"${var.region_instance_count[var.region_name]}\"<\/pre>\n<h3>variables.tf<\/h3>\n<pre class=\"lang:default decode:true \">variable \"region_instance_count\" {\r\n  type = \"map\"\r\n  default = {\r\n    us-east-1 = 2\r\n    us-west-2 = 1\r\n    eu-central-1 = 1\r\n    eu-west-1 = 1\r\n  }\r\n}\r\n<\/pre>\n<p>It also supports split if you want to multi-value a string variable.<\/p>\n<p>Another couple things before I forget, Terraform apply, doesn&#8217;t just set up new infrastructure, it also can be used to modify existing infrastructure, which is a very powerful feature. So if you have something deployed and want to make a change, terraform apply is your friend.<\/p>\n<p>And finally, when you are done with the infrastructure you spun up or it&#8217;s time to bring her down&#8230; &#8216;terraform destroy&#8217;<\/p>\n<pre class=\"lang:default decode:true \">\u279c  jump terraform destroy\r\nDo you really want to destroy?\r\n  Terraform will delete all your managed infrastructure.\r\n  There is no undo. Only 'yes' will be accepted to confirm.\r\n\r\n  Enter a value: yes\r\n\r\nvar.aws_access_key_id\r\n  Enter a value: blahblah\r\n\r\nvar.aws_secret_access_key\r\n  Enter a value: blahblahblahblah\r\n\r\n...\r\n\r\nDestroy complete! Resources: 3 destroyed.\r\n\u279c  jump<\/pre>\n<p>I hope this article helps.<\/p>\n<p>Happy Terraforming \ud83d\ude09<\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<a href=\"https:\/\/tuxlabs.com\/?p=486\" rel=\"bookmark\" title=\"Permalink to How To: Launch A Jump Host In AWS Using Terraform\"><p>I have been a Hashicorp fan boy for a couple\u00a0of years now. I am impressed, and happy with pretty much everything they have done from Vagrant to Consul and more. In short they make the DevOps world a better place. That being said this article is about the aptly named Terraform product. Here is how [&hellip;]<\/p>\n<\/a>","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[131,130,1,78,12],"tags":[23,175,139,137,174,171,160,173,172],"class_list":{"0":"post-486","1":"post","2":"type-post","3":"status-publish","4":"format-standard","6":"category-aws","7":"category-cloud","8":"category-howtos","9":"category-security","10":"category-systems-administration","11":"tag-aws","12":"tag-bastion","13":"tag-ec2","14":"tag-hashicorp","15":"tag-jump-host","16":"tag-mongodb","17":"tag-security","18":"tag-security-group","19":"tag-terraform","20":"h-entry","21":"hentry"},"_links":{"self":[{"href":"https:\/\/tuxlabs.com\/index.php?rest_route=\/wp\/v2\/posts\/486","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tuxlabs.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tuxlabs.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tuxlabs.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/tuxlabs.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=486"}],"version-history":[{"count":16,"href":"https:\/\/tuxlabs.com\/index.php?rest_route=\/wp\/v2\/posts\/486\/revisions"}],"predecessor-version":[{"id":557,"href":"https:\/\/tuxlabs.com\/index.php?rest_route=\/wp\/v2\/posts\/486\/revisions\/557"}],"wp:attachment":[{"href":"https:\/\/tuxlabs.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=486"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tuxlabs.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=486"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tuxlabs.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=486"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}