I am new to terraform. I have a Dockerized application that can be deployed using a docker-compose file.
I wrote a terraform script that creates a a security group, an EC2 machine, and runs a script that downloads docker and docker-compose, and I am trying to upload this docker-compose file from the local machine to the remote one. Whenever terraform reaches this step, it generates the following error:
aws_instance.docker_swarm: Provisioning with 'file'...
Error: host for provisioner cannot be empty
Below is the terraform template:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 2.70"
}
}
}
provider "aws" {
profile = var.profile
region = var.region
}
resource "aws_security_group" "allow_ssh_http" {
name = "allow_ssh_http"
description = "Allow SSH and HTTP access to ports 22 and 1337"
ingress {
description = "SSH from everywhere"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "Access Port 1337 from Everywhere"
from_port = 1337
to_port = 1337
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"]
}
tags = {
Name = "allow_ssh_http"
}
}
resource "aws_instance" "docker_swarm" {
ami = var.amis[var.region]
instance_type = var.instance_type
key_name = var.key_name
vpc_security_group_ids = [aws_security_group.allow_ssh_http.id]
user_data = "${file("deployServices.sh")}"
provisioner "file" {
source = "./docker-compose.yml"
destination = "/home/ubuntu/docker-compose.yml"
}
tags = {
Name = "NK Microservices Stack"
}
}
output "ec2_id" {
value = aws_instance.docker_swarm.id
}
output "ec2_ip" {
value = aws_instance.docker_swarm.public_ip
}
I think you will need to provide connection details to the provisioner. For example:
resource "aws_instance" "docker_swarm" {
ami = var.amis[var.region]
instance_type = var.instance_type
key_name = var.key_name
vpc_security_group_ids = [aws_security_group.allow_ssh_http.id]
user_data = "${file("deployServices.sh")}"
provisioner "file" {
source = "./docker-compose.yml"
destination = "/home/ubuntu/docker-compose.yml"
connection {
host = self.public_ip
user = "ubuntu"
private_key = file("<path-to-private-ssh-key>")
}
}
tags = {
Name = "NK Microservices Stack"
}
}
where <path-to-private-ssh-key> is the ssh key associated with your var.key_name.
The Terraform documentation states that you should only use provisioners as a last resort.
User Data can contain files in addition to a startup script. I would definitely recommend that approach before trying to use a provisioner. Or you could even include the file as a heredoc in the user data script.
Alternatively, you could copy the file to S3 before running Terraform, or even use Terraform to upload the file to S3, and then simply have the EC2 instance download the file as part of the user data script.
Related
Is it possible to launch multiple ec2 instances from terraform using a single VPC? I'm building something which requires multiple instances to be launched from the same region and I'm doing all this using Terraform. But there's a limit in AWS VPC: per region only 5 VPCs are allowed. What I've been doing until now is each time when I need to launch an instance I create a separate VPC for it in terraform. Below is the code for reference:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.0"
}
}
}
# Configure the AWS Provider
provider "aws" {
region = "us-east-2"
access_key = "XXXXXXXXXXXXXXXXX"
secret_key = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
}
# 1. Create vpc
resource "aws_vpc" "prod-vpc" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "production"
}
}
# 2. Create Internet Gateway
resource "aws_internet_gateway" "gw" {
vpc_id = aws_vpc.prod-vpc.id
}
# 3. Create Custom Route Table
resource "aws_route_table" "prod-route-table" {
vpc_id = aws_vpc.prod-vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.gw.id
}
route {
ipv6_cidr_block = "::/0"
gateway_id = aws_internet_gateway.gw.id
}
tags = {
Name = "Prod"
}
}
# 4. Create a Subnet
resource "aws_subnet" "subnet-1" {
vpc_id = aws_vpc.prod-vpc.id
cidr_block = "10.0.1.0/24"
availability_zone = "us-east-2a"
tags = {
Name = "prod-subnet"
}
}
# 5. Associate subnet with Route Table
resource "aws_route_table_association" "a" {
subnet_id = aws_subnet.subnet-1.id
route_table_id = aws_route_table.prod-route-table.id
}
# 6. Create Security Group to allow port 22,80,443
resource "aws_security_group" "allow_web" {
name = "allow_web_traffic"
description = "Allow Web inbound traffic"
vpc_id = aws_vpc.prod-vpc.id
ingress {
description = "HTTPS"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "self"
from_port = 8000
to_port = 8000
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "SSH"
from_port = 22
to_port = 22
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"]
}
tags = {
Name = "allow_web"
}
}
# 7. Create a network interface with an ip in the subnet that was created in step 4
resource "aws_network_interface" "web-server-nic" {
subnet_id = aws_subnet.subnet-1.id
private_ips = ["10.0.1.50"]
security_groups = [aws_security_group.allow_web.id]
}
# 8. Assign an elastic IP to the network interface created in step 7
resource "aws_eip" "one" {
vpc = true
network_interface = aws_network_interface.web-server-nic.id
associate_with_private_ip = "10.0.1.50"
depends_on = [aws_internet_gateway.gw]
}
output "server_public_ip" {
value = aws_eip.one.public_ip
}
# 9. Create Ubuntu server and install/enable apache2
resource "aws_instance" "web-server-instance" {
ami = var.AMI_ID
instance_type = "g4dn.xlarge"
availability_zone = "us-east-2a"
key_name = "us-east-2"
network_interface {
device_index = 0
network_interface_id = aws_network_interface.web-server-nic.id
}
root_block_device {
volume_size = "200"
}
iam_instance_profile = aws_iam_instance_profile.training_profile.name
depends_on = [aws_eip.one]
user_data = <<-EOF
#!/bin/bash
python3 /home/ubuntu/setting_instance.py
EOF
tags = {
Name = var.INSTANCE_NAME
}
}
The only downside to this code is it creates separate VPC everytime I create an instance. I read in a stackoverflow post that we can import an existing VPC using terraform import command. Along with the VPC, I had to import the Internet Gateway and Route Table as well (it was throwing error otherwise). But then I wasn't able to access the instance using SSH and also the commands in the user_data part didn't execute (setting_instance.py will send a firebase notification once the instance starts. That's the only purpose of setting_instance.py)
Not only VPC I'd also like to know if I can use the other resources as well to it's fullest extent possible.
I'm new to terraform and AWS. Any suggestions in the above code are welcome.
EDIT: Instances are created one at a time according to the need, i.e., whenever there is a need to create a new instance I use this code. In the current scenario if there are already 5 instances running up in a region then I won't be able to use this code to create a 6th instance in the same region when the demand arises.
If as you say, they would be exactly same, the easiest way would be to use count, which would indicate how many instance you want to have. For that you can introduce new variable:
variable "number_of_instance" {
default = 1
}
and then
resource "aws_instance" "web-server-instance" {
count = var.number_of_instance
ami = var.AMI_ID
instance_type = "g4dn.xlarge"
availability_zone = "us-east-2a"
key_name = "us-east-2"
network_interface {
device_index = 0
network_interface_id = aws_network_interface.web-server-nic.id
}
root_block_device {
volume_size = "200"
}
iam_instance_profile = aws_iam_instance_profile.training_profile.name
depends_on = [aws_eip.one]
user_data = <<-EOF
#!/bin/bash
python3 /home/ubuntu/setting_instance.py
EOF
tags = {
Name = var.INSTANCE_NAME
}
}
All this must be manage by same state file, not fully separate state files, as again you will end up with duplicates of the VPC. You only change number_of_instance to what you want. For more resilient solution, you would have to use autoscaling group for the instances.
I really can't figure out why I'm unable to SSH into my newly created EC2 instance and can't figure out why for the life of me.
Here is some of my code in Terraform where I created the EC2 and security groups for it.
This is my EC2 code
resource "aws_key_pair" "AzureDevOps" {
key_name = var.infra_env
public_key = var.public_ssh_key
}
# Create network inferface for EC2 instance and assign secruity groups
resource "aws_network_interface" "vm_nic_1" {
subnet_id = var.subnet_id
private_ips = ["10.0.0.100"]
tags = {
Name = "${var.infra_env}-nic-1"
}
security_groups = [
var.ssh_id
]
}
# Add elastic IP addresss for public connectivity
resource "aws_eip" "vm_eip_1" {
vpc = true
instance = aws_instance.virtualmachine_1.id
associate_with_private_ip = "10.0.0.100"
depends_on = [var.gw_1]
tags = {
Name = "${var.infra_env}-eip-1"
}
}
# Deploy virtual machine using Ubuntu ami
resource "aws_instance" "virtualmachine_1" {
ami = var.ami
instance_type = var.instance_type
key_name = aws_key_pair.AzureDevOps.id
#retrieve the Administrator password
get_password_data = true
connection {
type = "ssh"
port = 22
password = rsadecrypt(self.password_data, file("id_rsa"))
https = true
insecure = true
timeout = "10m"
}
network_interface {
network_interface_id = aws_network_interface.vm_nic_1.id
device_index = 0
}
user_data = file("./scripts/install-cwagent.ps1")
tags = {
Name = "${var.infra_env}-vm-1"
}
}
Here is the code for my security group
resource "aws_security_group" "ssh" {
name = "allow_ssh"
description = "Allow access to the instance via ssh"
vpc_id = var.vpc_id
ingress {
description = "Access the instance via ssh"
from_port = 22
to_port = 22
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"]
}
tags = {
Name = "${var.infra_env}-allow-ssh"
}
}
If I need to provide any more code or information I can, it's my first time trying to do this and it's frustrating trying to figure it out. I'm trying to use Putty as well and not sure if I just don't know how to use it correctly or if it's something wrong with my EC2 configuration.
I used my public ssh key from my computer for the variable in my aws_key_pair resource. I saved my public ssh key pair as a .ppk file for putty and on my aws console when I go to "connect" it says to use ubuntu#10.0.0.100 for my host name in Putty which I did and when I click okay and it tries to connect it gets a network error connection timed out
I used my public ssh key
You need to use your private key, not public.
use ubuntu#10.0.0.100
10.0.0.100 is private IP address. To be able to connect to your instance over the internet you need to use public IP address.
I am planning to create puppet infrastructure on AWS with a server node and agent nodes. To be able to install the puppet server and puppet agent packages, it is required to add the IP's to the hosts file on each of the nodes as below
[puppet master ip] puppetmaster puppet
[puppet client ip] puppetclient
I understand that remote-exec runs after the resource is created. But i want it to run after all the resources are created so that i can get the information about the master and client IP's together. So, i used a null resource to execute the remote-exec provisioner after the instances are created. But I am running into an issue
it says This object does not have an attribute named "public_ip"
I have tried using id instead of public_ip and it complains that this object does not have an attribute named "id"
I keep master IP constant and loop over only the agent IP's to be able to put them both into the /etc/hosts file for each of the instance. Is my approach correct in the remote-exec part?
here is the full code
provision_ec2.tf
resource "aws_key_pair" "puppet" {
key_name = "puppet"
public_key = file("puppet.pub")
}
# Provision Puppet Server
resource "aws_instance" "puppet_instances" {
for_each = toset(local.expanded_names)
ami = var.ami
instance_type = var.instance_type
key_name = aws_key_pair.puppet.key_name
security_groups = ["${aws_security_group.puppet-server.name}"]
tags = {
Name = each.value
}
}
resource "null_resource" "provisioner" {
count = "${var.total_count}"
triggers = {
master_ip = "${element(aws_instance.puppet_instances.*.public_ip, count.index)}"
}
connection {
host = "${element(aws_instance.puppet_instances.*.public_ip, count.index)}"
type = "ssh"
user = "ubuntu"
private_key = file("puppet")
}
# set hostname
provisioner "remote-exec" {
inline = [
"sudo echo ${element(aws_instance.puppet_instances.*.public_ip,0)} puppetmaster puppet>>/etc/hosts",
"sudo echo ${element(aws_instance.puppet_instances.*.public_ip, count.index)} puppetclient>>/etc/hosts"
]
}
}
locals {
expanded_names = flatten([
for name, count in var.host_name : [
for i in range(count) : format("%s-%02d", name, i+1)
]
])
}
variables.tf
# Set the instances type. For puppet, choose instance with minimum 2GB RAM
variable "instance_type" {
default = "t2.small"
}
# Set the region in which the instances will be provisioned. Default region is N.Virgina
variable "aws_region" {
default = "us-east-1"
}
# Sepcify the AMI to be used
variable "ami" {
default = "ami-09e67e426f25ce0d7"
}
# Set the hostnames for the instances as per the count
variable "host_name" {
type = map(number)
default = {
"Puppet-Server" = 1
"Puppet-Agent" = 3
}
}
variable "total_count" {
default = 4
}
security_groups.tf
# Create Security Groups
resource "aws_security_group" "puppet-server" {
name = "puppet-server-SG"
description = "Security Group for puppet master instance"
ingress {
from_port = "0"
to_port = "0"
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = "0"
to_port = "0"
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "puppet-server-SG"
}
}
I am out of ideas on how to achieve this.
Since you are using for_each, you should use values first. So it should be:
master_ip = element(values(aws_instance.puppet_instances)[*].public_ip, count.index)
I am creating ec2 instance and this is my main.tf file
variable "aws_key_pair" {
default = "~/aws/aws_keys/terraform-ec2.pem"
}
provider "aws" {
region = "us-east-1"
version = "~>2.46"
}
resource "aws_security_group" "http_server_sg" {
name = "http_server_sg"
vpc_id = "vpc-c5f40fb8"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 22
to_port = 22
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"]
}
tags = {
name = "http_server_sg"
}
}
resource "aws_instance" "http_server" {
ami = "ami-0947d2ba12ee1ff75"
key_name = "terraform-ec2"
instance_type = "t2.micro"
vpc_security_group_ids = [aws_security_group.http_server_sg.id]
subnet_id = "subnet-1169c14e"
connection {
type = "ssh"
host = self.public_ip
user = "ec2_user"
private_key = file(var.aws_key_pair)
}
provisioner "remote_exec" {
inline = [
"sudo yum install httpd -y",
"sudo service httpd start",
"echo Welcome to virtual server setup by terraform , IP address ${self.public_dns} | sudo tee /var/www/html/index.html"
]
}
}
When I am running : terraform apply I am getting following error
Could not load plugin
Plugin reinitialization required. Please run "terraform init".
Plugins are external binaries that Terraform uses to access and manipulate
resources. The configuration provided requires plugins which can't be located,
don't satisfy the version constraints, or are otherwise incompatible.
Terraform automatically discovers provider requirements from your
configuration, including providers used in child modules. To see the
requirements and constraints, run "terraform providers".
Failed to instantiate provisioner "remote_exec" to obtain schema: unknown
provisioner "remote_exec"
But I have already done terraform init and when I am running terraform validate I am getting same above error
It's "remote-exec" ...
In terraform/aws/global/vpc/security_groups.tf I have the below code to create my bastion security group, and the output.tf file as well which is below. But in terraform/aws/layers/bastion/main.tf (code also below) I reference that security group as I need its security group ID to create my EC2 instance, the issue I have is that rather than getting the ID from the already existing security group created by the /vpc/security_groups.tf config it tries to create the whole security group and the run obviously fails because it already exists. How can I change my code to get the ID of the existing SG? I don't want to create my SG in the same config file as my instance, some of my security groups are shared between different resources. I am using Terraform Cloud and VPC has its own workspace, so I assume this could actually be an issue with the states being different.. is there a work around for this?
terraform/aws/global/vpc/security_groups.tf
provider "aws" {
region = "eu-west-1"
}
resource "aws_security_group" "bastion" {
name = "Bastion_Terraform"
description = "Bastion SSH access Terraform"
vpc_id = "vpc-12345"
ingress {
description = "Bastion SSH"
from_port = ##
to_port = ##
protocol = "##"
cidr_blocks = ["1.2.3.4/56"]
}
ingress {
description = "Bastion SSH"
from_port = ##
to_port = ##
protocol = "##"
cidr_blocks = ["1.2.3.4/0"]
}
egress {
description = "Access to "
from_port = ##
to_port = ##
protocol = "tcp"
security_groups = ["sg-12345"]
}
egress {
description = "Access to ##"
from_port = ##
to_port = ##
protocol = "tcp"
security_groups = ["sg-12345"]
}
tags = {
Name = "Bastion Terraform"
}
}
terraform/aws/global/vpc/outputs.tf
output "bastion-sg" {
value = aws_security_group.bastion.id
}
terraform/aws/layers/bastion/main.tf
provider "aws" {
region = var.region
}
module "vpc" {
source = "../../global/vpc"
}
module "ec2-instance" {
source = "terraform-aws-modules/ec2-instance/aws"
name = "bastion"
instance_count = 1
ami = var.image_id
instance_type = var.instance_type
vpc_security_group_ids = ["${module.vpc.bastion-sg}"]
subnet_id = var.subnet
iam_instance_profile = var.iam_role
tags = {
Layer = "Bastion"
}
}
When you have a child module block like this in a TF module:
module "ec2-instance" {
source = "terraform-aws-modules/ec2-instance/aws"
name = "bastion"
instance_count = 1
ami = var.image_id
instance_type = var.instance_type
vpc_security_group_ids = ["${module.vpc.bastion-sg}"]
subnet_id = var.subnet
iam_instance_profile = var.iam_role
tags = {
Layer = "Bastion"
}
}
It doesn't just reference that child module, it instatiates a completely new instance of it unique only to the parent module and its state. Think of this not like an assignment or a pointer but the construction of a whole new instance of the module (using the module as a template) with all of its resources created again.
You will need to either directly reference the outputs of the child module in the parent module that has its module block or you will need to use a terraform_remote_state data source or Terragrunt dependency to load the outputs from the state file.