hashicorp / terraform-provider-aws

The AWS Provider enables Terraform to manage AWS resources.
https://registry.terraform.io/providers/hashicorp/aws
Mozilla Public License 2.0
9.8k stars 9.15k forks source link

How can I access IP from aws_ecs_service using `assign_public_ip` #3444

Open eschwartz opened 6 years ago

eschwartz commented 6 years ago

I'm creating a ECS service with launch_type = "FARGATE" and assign_public_ip = true.

I would like to be able to access the public IP of the service after it's created, so that I could create a Route53 record. It doesn't look like there is any way currently to accomplish this.

Could we add a public_ip attribute to the aws_ecs_service resource?

Thanks!

Terraform Version

Terraform v0.11.3
+ provider.aws v1.9.0

Affected Resource(s)

Terraform Configuration Files

resource "aws_ecs_service" "mysvc" {
  name = "my_svc"
  cluster = "${aws_ecs_cluster.ecs_cluster.arn}"
  task_definition = "${aws_ecs_task_definition.my_task.arn}"
  desired_count = 1
  launch_type = "FARGATE"

  network_configuration {
    subnets = ["${local.subnet_id}"]
    assign_public_ip = true
    security_groups = ["${aws_security_group.my_sg.id}"]
  }
}

Debug Output

N/A

Panic Output

N/A

Expected Behavior

N/A

Actual Behavior

N/A

Steps to Reproduce

N/A

Important Factoids

N/A

References

loivis commented 6 years ago
  1. Information of public ip is not included in DescribeServices api response. We might need some custom logic to export the attribute.

  2. Curious about your user case. The public ip seems like dynamic. Do you plan another trigger to update dns record on each ecs task change?

eschwartz commented 6 years ago

Curious about your user case. The public ip seems like dynamic. Do you plan another trigger to update dns record on each ecs task change?

I'm trying to create a route53 record, to point at the public IP. So yes, whenever the ECS task changes, it would update the Route53 record

redbelow commented 6 years ago

would be nice to have private_ip of host as well, i need that for setting an environment variable (local host ip) to a container.

dot1q commented 6 years ago

+1

unacceptable commented 6 years ago

I am running into a very similar issue when attempting to create an aws_lb_target_group_attachment resource. Perhaps I am missing something there though.

EDIT:

I was missing the fact that with Fargate you don't have to specify aws_lb_target_group_attachment because it is specified in aws_ecs_service (facepalm).

As for this original issue:

You can have a Fargate aws_ecs_service with multiple containers so I am not sure that public_ip would be a valid attribute. Even if there was a public_ips attribute added, then there would still be the issue that everytime a container was deprovisioned you would have to do another terraform apply so that your new container was recognized by route53.

Here are a couple of solutions though:

  1. Create an alb and make an alias record in route53. This is what I am doing. (Not ideal if you are worried about the cost for one container, but then again why use Fargate if that's the case).
  2. Create a lambda function to get the container's IP address and update route53 via boto3. (Ideal for cost-effective one-off solutions.)

Let me know if you need any help with the above solutions, and I would be happy to assist.

r1w1s1 commented 5 years ago

Curious about your user case. The public ip seems like dynamic. Do you plan another trigger to update dns record on each ecs task change?

I'm trying to create a route53 record, to point at the public IP. So yes, whenever the ECS task changes, it would update the Route53 record

nice! +1

dbogen commented 5 years ago

This would be really useful when creating a running task in Fargate via a service. Was hoping that maybe there would be a data source associated with the ECS cluster and the associated tasks in it that would provide this information. But there is not.

lostbearlabs commented 5 years ago

I have a related use case. I am trying to deploy a grafana instance in Fargate and then use the terraform grafana provider to configure it. I need a way to pass the IP address of the instance to the provider so that the provider will know what it's configuring.

fardin01 commented 4 years ago

No news about this?

tjtaill commented 4 years ago

Not sure it helps but I am looking to do this as well and my workaround plan is to use this https://github.com/matti/terraform-shell-resource

with ecs-cli ps command to get the ip into terraform https://docs.aws.amazon.com/AmazonECS/latest/developerguide/cmd-ecs-cli-ps.html

pixelicous commented 4 years ago

Having the same issue with fargate.

Whenever I update ECS or Task Definition (SSM parameters and such) I need to point my routing to this new public ip

however as @loivis its on Amazon's side

scott-doyland-burrows commented 4 years ago

Just wanted to add that I would also like to get the public IP from the task. In my case it doesn't matter that the IP changes.

Via terraform, I spin up the task, spin up a VM, and then there is an app on the VM that has to connect to the tasks public IP.

Both the task and VM are brought up/down at the same time.

I want to just pass the public IP directly to my terraform VM code.

phil-smarsh commented 4 years ago

would be nice to have private_ip of host as well, i need that for setting an environment variable (local host ip) to a container.

Is there any update on how i can access the private ip? I'm trying to send custom metrics to a datadog sidecar and can't seem to figure out how to get the private ip once the task definition is running.

scott-doyland-burrows commented 4 years ago

Hi, I worked around the issue of obtaining the public and private IPs by:

  1. Define a new security group for the ECS service
  2. Define the ECS service and opt to put it in the new security group
  3. The security group will automatically have a network interface attached to it which is owned by the service
  4. Pull out the private and public IPs from the network interface

It is a bit of a faff, you have to make sure you only have the single ECS service using the security group, otherwise you have multiple network interfaces and you cannot determine which one is for the ECS service via terraform.

It takes a few seconds for the network interface to be created. Terraform reports the ECS service resource creation is complete even though the network interface will not yet be available. So you need to set a "sleep" step in terraform (I waited 30 secs) before querying the network interface.

Also (and I cannot remember the exact issue as it was a while ago I tested this), if your ECS service has issues and keeps going up and down, it will keep getting a new public IP each time (and IIRC it deletes and redefines the network interface each time). If you decide to run a terraform apply/destroy at this point, then depending on timing, terraform can get confused as the interface it thinks is there, will not be.

I have the code I used, you are welcome to it, but the code/workaround isn't really suitable for production use due to the issue of changing network interfaces and IPs when the ECS service restarts. But then that's an AWS "feature".

Scott

phil-smarsh commented 4 years ago

Thank you @scott-doyland-burrows ! It turns out that my issue wasn't really an issue and a lapse in my understanding of networking in Fargate.

With multiple containers in the same Fargate task, you can use localhost/127.0.0.1 to communicate between containers.

ahmadRMusa commented 3 years ago

Hi, I worked around the issue of obtaining the public and private IPs by:

  1. Define a new security group for the ECS service
  2. Define the ECS service and opt to put it in the new security group
  3. The security group will automatically have a network interface attached to it which is owned by the service
  4. Pull out the private and public IPs from the network interface

It is a bit of a faff, you have to make sure you only have the single ECS service using the security group, otherwise you have multiple network interfaces and you cannot determine which one is for the ECS service via terraform.

It takes a few seconds for the network interface to be created. Terraform reports the ECS service resource creation is complete even though the network interface will not yet be available. So you need to set a "sleep" step in terraform (I waited 30 secs) before querying the network interface.

Also (and I cannot remember the exact issue as it was a while ago I tested this), if your ECS service has issues and keeps going up and down, it will keep getting a new public IP each time (and IIRC it deletes and redefines the network interface each time). If you decide to run a terraform apply/destroy at this point, then depending on timing, terraform can get confused as the interface it thinks is there, will not be.

I have the code I used, you are welcome to it, but the code/workaround isn't really suitable for production use due to the issue of changing network interfaces and IPs when the ECS service restarts. But then that's an AWS "feature".

Scott

@scott-doyland-burrows I am new to Terraform, can you put a sample code for pulling he network interface IPs?

pandayankush commented 3 years ago

Hi, I have the same requirement. I need to extract the Private Ip of the Fargate task and need to pass the same in the container template as a parameter to the COMMAND so that the application can be run on that IP. I am trying the below command to run consul but it asks for IP.

"command": [ "consul agent -server -data-dir=/consul/data -bootstrap -ui -client=0.0.0.0 -bind=0.0.0.0" ],

Instead of 0.0.0.0 need to pass the private IP address, and couldn't find any solution yet.

scott-doyland-burrows commented 3 years ago

It was a few months ago so I cannot remember exactly how this works - as I have not had to touch it since then...

Create a security group (not shown below - but I did this via a module).

Create your ecs service (not shown below - but I did this via a module) and put in in the security group.

Then wait 30 seconds (required as it takes a few secs for the NIC to be defined). The NIC will be tied to a security group.

Then call the first data block below, this will get a list of all NICs in the security group (you must have just ONE NIC per security group for this to work). So you end up with one NIC ID.

The second data block gets the attributes for the ID of the NIC from the first data block.

The first output block displays the private IP of the NIC.

The second output block displays the public IP of the NIC.

resource "time_sleep" "sigserv_30_seconds" {
  depends_on = [module.ecsservicesigserv]

  create_duration = "30s"
}

data "aws_network_interfaces" "networkinterfacesigserv" {
  depends_on = [time_sleep.sigserv_30_seconds]

  filter {
    name   = "group-id"
    values = [module.securitygroupsigserv.security_group_id]
  }
}

data "aws_network_interface" "networkinterfacesigserv" {
  id = join(",", data.aws_network_interfaces.networkinterfacesigserv.ids)
}

output "ecs_privateipv4_sigserv" {
  value = data.aws_network_interface.networkinterfacesigserv.private_ip
}

output "ecs_publicipv4_sigserv" {
  value = join(",", data.aws_network_interface.networkinterfacesigserv[*].association[0].public_ip)
}

The problem you will have is that if the ecs service dies and restarts itself, it will get a different public IP (IIRC) so you would need to manage this by rerunning terraform, so I think you'd need a terraform apply to force terraform to pick up the new IP in terraform output.

I sort of gave up with this in the end as it was all a bit messy, it was fine for just some dev work, but I wouldn't suggest you use it for more than that.

timhaley94 commented 3 years ago

I have this problem and am using @scott-doyland-burrows solution.

My only addition is that instead of using a 30 second timeout, I am listing that the aws_network_interfaces data source depends_on my aws_ecs_service resource, and I have wait_for_steady_state set to true on my aws_ecs_service resource.

I believe this requires that the tasks reach a steady state, and thus be assigned eni's, before this data source is executed.

akramfstg commented 3 years ago

It was a few months ago so I cannot remember exactly how this works - as I have not had to touch it since then...

Create a security group (not shown below - but I did this via a module).

Create your ecs service (not shown below - but I did this via a module) and put in in the security group.

Then wait 30 seconds (required as it takes a few secs for the NIC to be defined). The NIC will be tied to a security group.

Then call the first data block below, this will get a list of all NICs in the security group (you must have just ONE NIC per security group for this to work). So you end up with one NIC ID.

The second data block gets the attributes for the ID of the NIC from the first data block.

The first output block displays the private IP of the NIC.

The second output block displays the public IP of the NIC.

resource "time_sleep" "sigserv_30_seconds" {
  depends_on = [module.ecsservicesigserv]

  create_duration = "30s"
}

data "aws_network_interfaces" "networkinterfacesigserv" {
  depends_on = [time_sleep.sigserv_30_seconds]

  filter {
    name   = "group-id"
    values = [module.securitygroupsigserv.security_group_id]
  }
}

data "aws_network_interface" "networkinterfacesigserv" {
  id = join(",", data.aws_network_interfaces.networkinterfacesigserv.ids)
}

output "ecs_privateipv4_sigserv" {
  value = data.aws_network_interface.networkinterfacesigserv.private_ip
}

output "ecs_publicipv4_sigserv" {
  value = join(",", data.aws_network_interface.networkinterfacesigserv[*].association[0].public_ip)
}

The problem you will have is that if the ecs service dies and restarts itself, it will get a different public IP (IIRC) so you would need to manage this by rerunning terraform, so I think you'd need a terraform apply to force terraform to pick up the new IP in terraform output.

I sort of gave up with this in the end as it was all a bit messy, it was fine for just some dev work, but I wouldn't suggest you use it for more than that.

Hi Scott, I like the way you managed to get the public ip but it did not work for me unfortunately. Getting the following error despite both network interfaces are well created there in both availability zones:

 Error: InvalidNetworkInterfaceID.NotFound: The networkInterface ID 'eni-0000e25458ba6b1a6,eni-0b4bb8cac218221ab' does not exist
│       status code: 400, request id: 172695fa-5f7c-4eea-acb4-977893ed695d
│ 
│   with data.aws_network_interface.nic_sg_ecs_service_test[0],
│   on transcoder.tf line 110, in data "aws_network_interface" "nic_sg_ecs_service_test":
│  110: data "aws_network_interface" "nic_sg_ecs_service_test" {

Woderning what is the issue here!

scott-doyland-burrows commented 3 years ago

Hi, The error suggests you are looking for an interface called eni-0000e25458ba6b1a6,eni-0b4bb8cac218221ab

So it looks like it finds two interfaces - and my original code will only work with one.

Scott

maximatt commented 2 years ago

Hi... I'm a newbie in terraform, but quickly I run into this issue and this is my recently workaround, that for now is "successfully enough" to me and perhaps could be useful to someone.

I have two services (_service1 and _service2) and _service2 depends from _service1 to retrieve some resources, where _service2 is defined like

resource "aws_ecs_task_definition" "aws-ecs-service_2-task" {
  :
  container_definitions = templatefile("${path.module}/${var.task_definitions}/service_2.json", {
    :
    service_1_ip        = file("${aws_ecs_service.aws-ecs-service_1.name}.ip")
  })
}

resource "aws_ecs_service" "aws-ecs-service_2" {
  :
  wait_for_steady_state= true
  depends_on = [aws_ecs_service.aws-ecs-service_1]
}

resource "null_resource" "get_ip" {
  provisioner "local-exec" {
    command     = "chmod +x ./get_ip.sh; ./get_ip.sh ${aws_ecs_cluster.aws-ecs-cluster.name} ${aws_ecs_service.aws-ecs-service_1.name}"
    interpreter = ["bash", "-c"]
  }
}

So, the script _getip.sh has the following content (I didnt try if its possible return the value to variable service_1_ip in a direct manner):

task_arn=$(aws ecs list-tasks --cluster $1 --service-name $2 --query 'taskArns[0]' --output text)
task_details=$(aws ecs describe-tasks --cluster $1 --task ${task_arn} --query 'tasks[0].attachments[0].details')
service_ip=$(echo $task_details | jq -r -c '.[] | select(.name=="privateIPv4Address").value')
echo -n $service_ip > $2.ip

To update _service2

terraform plan -input=false -replace=aws_ecs_task_definition.aws-ecs-service_2-task
terraform apply -input=false -auto-approve

In resume thats all... and I'm sure that this has a lot of issues and limitations (that I will be discovering time by time, but for now..."works")

Regards

jimsmith commented 1 year ago

I stumbled across this today and found it disappointing that more than 5 years this is still a thing :(

The use case is a single fargate container running so for now https://github.com/hashicorp/terraform-provider-aws/issues/3444#issuecomment-891760549 has given me the IP addresses required.

I hope to see this no longer a thing soon...

tatliHU commented 1 year ago

I found a better solution using tags: 1) Set enable_ecs_managed_tags = true in your aws_ecs_service. This will tag the network interface with aws:ecs:serviceName = aws:ecs:clusterName = 2) Use tag filter to identify the network interface

data "aws_network_interface" "interface_tags" {
  filter {
    name   = "tag:aws:ecs:serviceName"
    values = ["my_service_name"]
  }
}

output "public_ip" {
    value = data.aws_network_interface.interface_tags.association[0].public_ip
}

This is a robust approach as there can't be multiple network interfaces listed for your use-case.

dverzolla commented 11 months ago

I found a better solution using tags:

1. Set `enable_ecs_managed_tags = true` in your aws_ecs_service. This will tag the network interface with
   aws:ecs:serviceName = <aws_ecs_service.yourservice.name>
   aws:ecs:clusterName  = <aws_ecs_service.yourservice.cluster>

2. Use tag filter to identify the network interface
data "aws_network_interface" "interface_tags" {
  filter {
    name   = "tag:aws:ecs:serviceName"
    values = ["my_service_name"]
  }
}

output "public_ip" {
    value = data.aws_network_interface.interface_tags.association[0].public_ip
}

This is a robust approach as there can't be multiple network interfaces listed for your use-case.

This is the best approach. I've set wait_for_steady_state = true and output the value properly.

To check if the tag was right propagated. aws ec2 describe-network-interfaces --filters Name=tag:aws:ecs:serviceName,Values=service_name

egoriwe999 commented 7 months ago

My solution with ECS Service + Terraform: https://www.linkedin.com/pulse/how-receive-public-ipv4-from-aws-ecs-via-terraform-egor-salo-l9a9e

tatliHU commented 7 months ago

My solution with ECS Service + Terraform: https://www.linkedin.com/pulse/how-receive-public-ipv4-from-aws-ecs-via-terraform-egor-salo-l9a9e

This is almost exactly my solution which is right above your comment. I'm happy that people use it but if you create a blog post about it on LinkedIn, you should tag me there.

egoriwe999 commented 7 months ago

My solution with ECS Service + Terraform: https://www.linkedin.com/pulse/how-receive-public-ipv4-from-aws-ecs-via-terraform-egor-salo-l9a9e

This is almost exactly my solution which is right above your comment. I'm happy that people use it but if you create a blog post about it on LinkedIn, you should tag me there.

oh, really, our solutions are the same, but I scrolled through your comment and found it manually(

narimantos commented 6 months ago

I found a better solution using tags:

1. Set `enable_ecs_managed_tags = true` in your aws_ecs_service. This will tag the network interface with
   aws:ecs:serviceName = <aws_ecs_service.yourservice.name>
   aws:ecs:clusterName  = <aws_ecs_service.yourservice.cluster>

2. Use tag filter to identify the network interface
data "aws_network_interface" "interface_tags" {
  filter {
    name   = "tag:aws:ecs:serviceName"
    values = ["my_service_name"]
  }
}

output "public_ip" {
    value = data.aws_network_interface.interface_tags.association[0].public_ip
}

This is a robust approach as there can't be multiple network interfaces listed for your use-case.

This is the best approach. I've set wait_for_steady_state = true and output the value properly.

To check if the tag was right propagated. aws ec2 describe-network-interfaces --filters > Name=tag:aws:ecs:serviceName,Values=service_name

This won't help if the fargate container crashes and will assign a new ip?

celesteking commented 6 months ago

robust approach as there can't be multiple network interfaces

@tatliHU, Doesn't work when there are multiple tasks and thus multiple interfaces. Is there other solution based on this one?

tatliHU commented 6 months ago

Hi @celesteking, it sure works for multiple different tasks as they will have different aws:ecs:serviceName tags. Now if you want to scale up the same task (i.e using the desired_count attribute) you can do two things: 1) use the same approach as the filter will list all the network interfaces. since they are the same app you can use any of the interfaces 2) use a loadbalancer (check out my solution https://github.com/tatliHU/webservers/blob/main/modules/ecs/main.tf)

celesteking commented 5 months ago

use the same approach as the filter will list all the network interfaces. since they are the same app you can use any of the interfaces

Doesn't work: │ Error: reading EC2 Network Interface: too many results: wanted 1, got 2

data "aws_network_interface" "netiface" {
  filter {
    name   = "tag:aws:ecs:serviceName"
    values = ["my-runner"]
  }
output "iface_public_ip" {
  description = "Public IP address of service"
  value = [(length(data.aws_network_interface.netiface) > 0) ? data.aws_network_interface.netiface.*.association[0].public_ip : "NONE"]
  }
tatliHU commented 5 months ago

@celesteking use aws_network_interfaces instead of aws_network_interface

celesteking commented 5 months ago

Max useful output it can return is the list of interface names, but I need per-interface data like private and public IPs. I'm new to Terraform. Something like: ifaces = { "my-runner-service": {"eni-123": {public: "1.2.3.4", private: "10.1.1.1"}, "eni-234": {public: "2.3.4.5", private: "10.2.2.2"}}}

celesteking commented 5 months ago
data "aws_network_interfaces" "interface_tags" {
  filter {
    name   = "tag:aws:ecs:serviceName"
    values = ["my-runner"]
  }
}

data "aws_network_interface" "netiface" {
  for_each = toset(data.aws_network_interfaces.interface_tags.ids)
  id = each.key
}

output "iface_ip_map" {
  description = "IP addresses of XX service"
  value       = { for k,v in data.aws_network_interface.netiface: k => {pub: v.association[0].public_ip, priv: v.private_ip }}
}

But that doesn't show which service has what task with what IP[s].