Unable to pass service annotations when deploying helm chart via terraform - terraform

I have seeing some examples regarding how to pass annotations when deploying a helm chart via terraform but none of then are working as expected, in this case, im trying to create a service assining a private ip on a specific subnet, but instead, its creating a public IP.
My terraform files:
locals {
helm_general = {
# Reference values
# https://github.com/elastic/helm-charts/blob/master/elasticsearch/values.yaml
elasticsearch = {
name = "elasticsearch"
chart = "elastic/elasticsearch"
tag = "7.14.0"
namespace = "elasticsearch"
set = [
{
name = "nodeSelector.agentpool"
value = "general"
},
{
name = "replicas"
value = "1"
},
{
name = "minimumMasterNodes"
value = "1"
},
{
name = "image"
value = "docker.elastic.co/elasticsearch/elasticsearch"
},
{
name = "imageTag"
value = "7.14.0"
},
{
name = "resources.requests.cpu"
value = "10m"
},
{
name = "resources.requests.memory"
value = "128Mi"
},
{
name = "volumeClaimTemplate.reosources.requests.storage"
value = "4Gi"
},
{
name = "persistence.enabled"
value = "false"
},
{
name = "service.type"
value = "LoadBalancer"
},
{
name = "service.annotations\\.service\\.beta\\.kubernetes\\.io/azure-load-balancer-internal"
value = "true"
},
{
name = "service.annotations\\.service\\.beta\\.kubernetes\\.io/azure-load-balancer-internal-subnet"
value = "somesubnet"
},
]
timeout = "900"
}
}
}
Helm deployment
resource "helm_release" "helm" {
provider = helm.general
for_each = local.helm_general
name = each.value.name
chart = each.value.chart
namespace = format(each.value.namespace)
dynamic "set" {
iterator = item
for_each = each.value.set == null ? [] : each.value.set
content {
name = item.value.name
value = item.value.value
}
}
depends_on = [kubernetes_namespace.general]
}
Plan / apply output
https://i.imgur.com/uAdmblM.png
And what is currently being deployed is a public ip instead of a private ip:
Namespace: elasticsearch
Labels: app=elasticsearch-master
app.kubernetes.io/managed-by=Helm
chart=elasticsearch
heritage=Helm
release=elasticsearch
Annotations: meta.helm.sh/release-name: elasticsearch
meta.helm.sh/release-namespace: elasticsearch
Selector: app=elasticsearch-master,chart=elasticsearch,release=elasticsearch
Type: LoadBalancer
IP Families: <none>
IP: xx
IPs: xxx
LoadBalancer Ingress: redacted public ip
Port: http 9200/TCP
TargetPort: 9200/TCP
NodePort: http 32083/TCP
Endpoints:
Port: transport 9300/TCP
TargetPort: 9300/TCP
NodePort: transport 32638/TCP
Endpoints:
Session Affinity: None
External Traffic Policy: Cluster
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal EnsuringLoadBalancer 1s service-controller Ensuring load balancer
Normal EnsuredLoadBalancer <invalid> service-controller Ensured load balancer
References that i have been following:
https://github.com/hashicorp/terraform-provider-helm/issues/125
https://registry.terraform.io/providers/hashicorp/helm/latest/docs/resources/release
Edit:
error message:
Error: unable to decode "": resource.metadataOnlyObject.ObjectMeta: v1.ObjectMeta.Annotations: ReadString: expects " or n, but found t, error found in #10 byte of ...|nternal":true},"labe|..., bigger context
..|beta.kubernetes.io/azure-load-balancer-internal":true},"labels":{"app":"elasticsearch-master","chart|...
with helm_release.helm["elasticsearch"],
on aks-general-helm.tf line 1, in resource "helm_release" "helm":
1: resource "helm_release" "helm" {

I just faced a similar issue, and here is what worked for me:
{
name = "service.annotations.service\\.beta\\.kubernetes\\.io/azure-load-balancer-internal"
value = "true"
},
I think the issue is how it is concatenated. The service in the chart manifest for elastic official is service.annotations:{} so you need to append .service then use \\.

if you have multiple you can try something like this, pass annotations as map and set them using
dynamic "set" {
for_each = var.ingress_annotations
content {
name = replace(set.key, ".", "\\.")
value = set.value
}
}

One way of doing this without the escape characters and keeping the original YAML format would be using values attribute of the helm_release resource. Would be curious to know if there was a specific used-case to not use it in the first place.
resource "helm_release" "helm" {
provider = helm.general
for_each = local.helm_general
name = each.value.name
chart = each.value.chart
namespace = format(each.value.namespace)
values = each.value.values ##CHANGE IS HERE ##
dynamic "set" {
iterator = item
for_each = each.value.set == null ? [] : each.value.set
content {
name = item.value.name
value = item.value.value
}
}
depends_on = [kubernetes_namespace.general]
}
The local in your case would be adjusted to below, you can still keep set for something which has a dependency on any terraform resources or with any other logical reasons.
locals {
helm_general = {
# Reference values
# https://github.com/elastic/helm-charts/blob/master/elasticsearch/values.yaml
elasticsearch = {
[...]
values = [file("${path.module}/elasticsearch-values.yaml")]
[...]
}
}
}
There has to be a new file elasticsearch-values.yaml at the same path (which can be adjusted with any relative path as per the local.helm_general.elasticsearch.values) location, where this terraform configurations exist.
# Reference values can be adapted as per the upstream chart.
# https://github.com/elastic/helm-charts/blob/master/elasticsearch/values.yaml
service:
annotations:
service.beta.kubernetes.io/azure-load-balancer-internal: "true"
service.beta.kubernetes.io/azure-load-balancer-internal-subnet: "somesubnet"

Related

Iterating over multi-level yaml values in terraform

I'm trying to shorten my terraform code for deploying azure vnets by iterating over values I provide in a yaml file. I want to write one .tf file with the code for the vnets, the subnets, the NSGs, etc. but I'm struggling to get the locals block right to correctly iterate through my yaml file (see below)
vnets:
- name: adds
location: eastus
address_space: ["10.1.0.0/24"]
subnets:
- name: adds
address_prefix: "10.1.0.0/27"
- name: dns
location: eastus
address_space: ["10.1.53.0/24"]
subnets:
- name: dns-inbound
address_prefix: "10.1.53.0/28"
- name: dns-outbound
address_prefix: "10.1.53.16/28"
Any help on how I should right my locals block would be appreciated!
This code will transform your yaml file into local map:
locals {
vnets = yamldecode(file("./test.yaml"))
vnets_map = {
for vnet in local.vnets.vnets :
vnet.name => {
address_space = vnet.address_space
location = vnet.location
subnets = {
for subnet in vnet.subnets :
subnet.name => subnet.address_prefix
}
}
}
}
output "example-output-dns-inbound-subnet" {
value = local.vnets_map.dns.subnets.dns-inbound
}
I took the liberty of changing lists to maps - it is better to navigate in terraform. Entire vnets_map object looks like this:
{
"adds" = {
"address_space" = [
"10.1.0.0/24",
]
"location" = "eastus"
"subnets" = {
"adds" = "10.1.0.0/27"
}
}
"dns" = {
"address_space" = [
"10.1.53.0/24",
]
"location" = "eastus"
"subnets" = {
"dns-inbound" = "10.1.53.0/28"
"dns-outbound" = "10.1.53.16/28"
}
}
}

Does the Terraform resource kubernetes_ingress_v1 have a "use_annotation" equivalent?

We're currently migrating our terraform kubernetes_ingress resource to a kubernetes_ingress_v1 resource. Previously, we had these annotations on the ingress:
annotations = {
"kubernetes.io/ingress.class" = "alb"
"alb.ingress.kubernetes.io/scheme" = "internet-facing"
"alb.ingress.kubernetes.io/certificate-arn" = var.create_acm_certificate ? aws_acm_certificate.eks_domain_cert[0].id : var.aws_acm_certificate_arn
"alb.ingress.kubernetes.io/listen-ports" = "[{\"HTTP\": 80}, {\"HTTPS\":443}]"
"alb.ingress.kubernetes.io/actions.ssl-redirect" = "{\"Type\": \"redirect\", \"RedirectConfig\": { \"Protocol\": \"HTTPS\", \"Port\": \"443\", \"StatusCode\": \"HTTP_301\"}}"
"alb.ingress.kubernetes.io/ssl-policy" = "ELBSecurityPolicy-TLS-1-2-Ext-2018-06"
"alb.ingress.kubernetes.io/healthcheck-path" = "/healthz"
}
along with this segment several times in the spec:
path {
backend {
service_name = "ssl-redirect"
service_port = "use-annotation"
}
path = "/*"
}
However, the kubernetes_ingress_v1 requires a format like:
path {
backend {
service {
name = "ssl-redirect"
port {
number = <number_value>
}
}
}
path = "/*"
}
where port is an actual number and not "use-annotation". Is there any way to replicate this "use-annotation" behavior in a kubernetes_ingress_v1 resource? Or, even better, is there a simpler way to handle this ssl-redirect rule in a kubernetes_ingress_v1?
You can achive that using the following sintaxis:
backend {
service {
name = "ssl-redirect"
port {
name = "use-annotation"
}
}
}
As you can see, you need to use the argument name instead port.

How to use tags from yaml file - terraform

I am trying to extract certain tags from a YAML file with Terraform, but i just don't know how.
Yaml file looks like this:
---
name: subscriptionName
emailContact: someemail#domain.com
tags:
- key: "tagKey1"
value: "tagValue1"
- key: "tagKey2"
value: "tagValue2"
- key: "tagKey3"
value: "tagValue3"
- key: "tagKey4"
value: "tagValue4"
What i am interested in is getting 2 (let's say key1 and key3) key-value pairs as tags and tag resouces. I know that 'locals' plays a role here, but i am kinda new to terraform and cannot get any resource tagged.
The resources are Azure (if it matters).
The resource i am trying to tag is:
resource "azurerm_network_security_group" "nsg" {
name = "randomname"
location = "westeurope"
resource_group_name = "random_rg"
tags { }
}
If you really want two random tags, you can use random_shuffle:
locals {
loaded_yaml = yamldecode(file("./your_file.yaml"))
}
resource "random_shuffle" "indices" {
input = range(0, length(local.loaded_yaml.tags))
result_count = 2
seed = timestamp()
}
output "random_tags" {
value = [for idx in random_shuffle.indices.result:
local.loaded_yaml.tags[idx]]
}
update
For example:
tags = {
(local.loaded_yaml.tags[0].key) = local.loaded_yaml.tags[0].value
(local.loaded_yaml.tags[3].key) = local.loaded_yaml.tags[3].value
}

Terraform - How can I reference a resource containing a "for_each" argument to another resource?

I created a resource that produces a list of VM's using for_each argument. I'm having trouble trying to reference this resource to my web_private_group.
resource "google_compute_instance_group" "web_private_group" {
name = "${format("%s","${var.gcp_resource_name}-${var.gcp_env}-vm-group")}"
description = "Web servers instance group"
zone = var.gcp_zone_1
network = google_compute_network.vpc.self_link
# I've added some attempts I've tried that do not work...
instances = [
//google_compute_instance.compute_engines.self_link
//[for o in google_compute_instance.compute_engines : o.self_link]
{for k, o in google_compute_instance.compute_engines : k => o.self_link}
//google_compute_instance.web_private_2.self_link
]
named_port {
name = "http"
port = "80"
}
named_port {
name = "https"
port = "443"
}
}
# Create Google Cloud VMs
resource "google_compute_instance" "compute_engines" {
for_each = var.vm_instances
name = "${format("%s","${var.gcp_resource_name}-${var.gcp_env}-each.value")}"
machine_type = "e2-micro"
zone = var.gcp_zone_1
tags = ["ssh","http"]
boot_disk {
initialize_params {
image = "debian-10"
}
}
}
variable "vm_instances" {
description = "list of VM's"
type = set(string)
default = ["vm1", "vm2", "vm3"]
}
How can I properly link my compute_engines to my web_private_group resource within the instances= [] block?
Edit: To further clarify, how can I state the fact that there are multiple instances within my compute_engines resource?
You probably just need to use a splat expression as follows:
instances = values(google_compute_instance.compute_engines)[*].id
Moreover, a for expression can be used as well:
instances = [for res in google_compute_instance.compute_engines: res.id]

Terraform how to get IP address of aws_lb

Question
If there a way to get the assigned IP address of an aws_lb resource at the time aws_lb is created by Terraform?
As in AWS documentation - NLB - To find the private IP addresses to whitelist, we can find out the IP address associated to ELB.
Open the Amazon EC2 console at https://console.aws.amazon.com/ec2/.
In the navigation pane, choose Network Interfaces.
In the search field, type the name of your Network Load Balancer.
There is one network interface per load balancer subnet.
On the Details tab for each network interface, copy the address from
Primary private IPv4 IP.
Background
To be able to setup security group to white list the ELB IP address as Network Load Balancer cannot not have Security Group as in Network Load Balancers don't have Security Groups.
Considered aws_network_interface but it does not work with an error.
Error: no matching network interface found
Also I think datasource assumes the resource already exists and cannot be used for the resource to be created by Terraform.
More elegent solution using only HCL in Terraform :
data "aws_network_interface" "lb" {
for_each = var.subnets
filter {
name = "description"
values = ["ELB ${aws_lb.example_lb.arn_suffix}"]
}
filter {
name = "subnet-id"
values = [each.value]
}
}
resource "aws_security_group" "lb_sg" {
vpc_id = var.vpc_id
ingress {
from_port = 0
to_port = 0
protocol = "tcp"
cidr_blocks = formatlist("%s/32", [for eni in data.aws_network_interface.lb : eni.private_ip])
description = "Allow connection from NLB"
}
}
Source : https://github.com/terraform-providers/terraform-provider-aws/issues/3007
Hope this helps.
The solution from #user1297406 leads to an exeption. data.aws_network_interface.lb is tuple with 2 elements. Correct syntax is:
data "aws_network_interface" "lb" {
count = length(var.vpc_private_subnets)
filter {
name = "description"
values = ["ELB ${aws_alb.lb.arn_suffix}"]
}
filter {
name = "subnet-id"
values = [var.vpc_private_subnets[count.index]]
}
}
resource "aws_security_group_rule" "lb_sg" {
from_port = 0
protocol = "TCP"
to_port = 0
type = "ingress"
cidr_blocks = formatlist("%s/32", data.aws_network_interface.lb.*.private_ip)
}
Using external provider
Get the NLB IP using Python/boto3 invoking from external provider.
nlb_private_ips.tf
variable "nlb_name" {
}
variable "vpc_id" {
}
variable "region" {
}
data "external" "get_nlb_ips" {
program = ["python", "${path.module}/get_nlb_private_ips.py"]
query = {
aws_nlb_name = "${var.nlb_name}"
aws_vpc_id = "${var.vpc_id}"
aws_region = "${var.region}"
}
}
output "aws_nlb_ip_decoded" {
value = "${jsondecode(data.external.get_nlb_ips.result.private_ips)}"
}
output "aws_nlb_ip_encoded" {
value = "${data.external.get_nlb_ips.result.private_ips}"
}
get_nlb_private_ips.py
import boto3
import json
import sys
def json_serial(obj):
"""JSON serializer for objects not serializable by default json code
Args:
obj: object to serialize into JSON
"""
_serialize = {
"int": lambda o: int(o),
"float": lambda o: float(o),
"decimal": lambda o: float(o) if o % 1 > 0 else int(o),
"date": lambda o: o.isoformat(),
"datetime": lambda o: o.isoformat(),
"str": lambda o: o,
}
return _serialize[type(obj).__name__.lower()](obj)
def pretty_json(dict):
"""
Pretty print Python dictionary
Args:
dict: Python dictionary
Returns:
Pretty JSON
"""
return json.dumps(dict, indent=2, default=json_serial, sort_keys=True, )
def get_nlb_private_ips(data):
ec2 = boto3.client('ec2', region_name=data['aws_region'])
response = ec2.describe_network_interfaces(
Filters=[
{
'Name': 'description',
'Values': [
"ELB net/{AWS_NLB_NAME}/*".format(
AWS_NLB_NAME=data['aws_nlb_name'])
]
},
{
'Name': 'vpc-id',
'Values': [
data['aws_vpc_id']
]
},
{
'Name': 'status',
'Values': [
"in-use"
]
},
{
'Name': 'attachment.status',
'Values': [
"attached"
]
}
]
)
# print(pretty_json(response))
interfaces = response['NetworkInterfaces']
# ifs = list(map(lamba index: interfaces[index]['PrivateIpAddresses'], xrange(len(interfaces))))
# --------------------------------------------------------------------------------
# Private IP addresses associated to an interface (ENI)
# Each association has the format:
# {
# "Association": {
# "IpOwnerId": "693054447076",
# "PublicDnsName": "ec2-52-88-47-177.us-west-2.compute.amazonaws.com",
# "PublicIp": "52.88.47.177"
# },
# "Primary": true,
# "PrivateDnsName": "ip-10-5-1-205.us-west-2.compute.internal",
# "PrivateIpAddress": "10.5.1.205"
# },
# --------------------------------------------------------------------------------
associations = [
association for interface in interfaces
for association in interface['PrivateIpAddresses']
]
# --------------------------------------------------------------------------------
# Get IP from each IP association
# --------------------------------------------------------------------------------
private_ips = [
association['PrivateIpAddress'] for association in associations
]
return private_ips
def load_json():
data = json.load(sys.stdin)
return data
def main():
data = load_json()
"""
print(data['aws_region'])
print(data['aws_vpc_id'])
print(data['aws_nlb_name'])
"""
ips = get_nlb_private_ips(data)
print(json.dumps({"private_ips": json.dumps(ips)}))
if __name__ == '__main__':
main()
Using aws_network_interfaces datasource
After aws_lb has been created.
data "aws_network_interfaces" "this" {
filter {
name = "description"
values = ["ELB net/${aws_lb.this.name}/*"]
}
filter {
name = "vpc-id"
values = ["${var.vpc_id}"]
}
filter {
name = "status"
values = ["in-use"]
}
filter {
name = "attachment.status"
values = ["attached"]
}
}
locals {
nlb_interface_ids = "${flatten(["${data.aws_network_interfaces.this.ids}"])}"
}
data "aws_network_interface" "ifs" {
count = "${length(local.nlb_interface_ids)}"
id = "${local.nlb_interface_ids[count.index]}"
}
output "aws_lb_network_interface_ips" {
value = "${flatten([data.aws_network_interface.ifs.*.private_ips])}"
}
i would suggest to use the data "dns_a_record_set" to get the IPs:
data "dns_a_record_set" "nlb_ips" {
host = aws_lb.<your_alb>.dns_name
}
You can find the documentation under https://registry.terraform.io/providers/hashicorp/dns/latest/docs/data-sources/dns_a_record_set

Resources