limited-guitar-4572
06/14/2024, 6:31 PMTo get the value of an Output[T] as an Output[str] consider:
1. o.apply(lambda v: f"prefix{v}suffix")
Our project is developed in Python and we are running the most recent versions of the pulumi-aws, pulumi-eks, pulumi-aws-native packages.salmon-gold-74709
06/15/2024, 8:31 AMaws-native
provider has a get_cluster
function that can fetch both the API endpoint and the certificate authority. However, Pulumi's asynchronous nature can sometimes lead to issues if resources are not created in the expected order. To ensure the cluster details are fetched correctly after the cluster is created, use Pulumi's .apply
method effectively.
1. *Using Pulumi's Command Provider:
2. When using the command provider, ensure the output is processed correctly to convert the Output
type to a string.
1. *User Data Template:
2. Properly structure the user data template to include the fetched EKS cluster details.
1. ### Example Implementation
1. Below is a Python-based example of how you can structure your Pulumi code to achieve this:
1. python
2. import pulumi
3. from pulumi_aws import ec2, eks
4. from pulumi_aws_native import eks as aws_native_eks
5. from pulumi_command import local
1. # Define your EKS cluster
2. cluster = eks.Cluster('my-cluster',
3. # Cluster configuration
4. )
1. # Fetch the cluster details using aws-native provider
2. cluster_details = aws_native_eks.get_cluster_output(
3. name=cluster.eks_cluster.name
4. )
1. # Function to process the Output[T] type and convert it to a string
2. def get_value_as_string(output):
3. return output.apply(lambda v: v)
1. cluster_endpoint = get_value_as_string(cluster_details.endpoint)
2. cluster_certificate = get_value_as_string(cluster_details.certificate_authority['data'])
1. # Define the user data template function
2. def create_user_data(cluster_name, endpoint, certificate):
3. user_data = f"""#!/bin/bash
4. /etc/eks/bootstrap.sh {cluster_name} --apiserver-endpoint '{endpoint}' --b64-cluster-ca '{certificate}'
5. """
6. return user_data
1. # Using the Pulumi apply method to create the user data string
2. user_data = pulumi.Output.all(cluster.eks_cluster.name, cluster_endpoint, cluster_certificate).apply(
3. lambda args: create_user_data(args[0], args[1], args[2])
4. )
1. # Define your launch template with the generated user data
2. launch_template = ec2.LaunchTemplate('my-launch-template',
3. name='my-launch-template',
4. user_data=user_data,
5. # Other configurations
6. )
1. # Define the node group using the launch template
2. node_group = eks.NodeGroup('my-node-group',
3. cluster=cluster.eks_cluster.name,
4. launch_template={
5. 'id': <http://launch_template.id|launch_template.id>,
6. 'version': '$Latest'
7. },
8. # Other configurations
9. )
10.
1. ### Explanation
1. *Cluster Definition:
2. The eks.Cluster
resource creates the EKS cluster.
1. *Fetching Cluster Details:
2. The aws_native_eks.get_cluster_output
function fetches the EKS cluster details and returns them as Output
types.
1. *Processing Outputs:
2. The get_value_as_string
function ensures the Output
values are converted to strings using the .apply
method.
1. *User Data Template:
2. The create_user_data
function creates the user data string by embedding the EKS cluster details.
1. *Launch Template:
2. The ec2.LaunchTemplate
resource defines the launch template for the EKS worker nodes using the generated user data.
1. *Node Group:
2. The eks.NodeGroup
resource uses the launch template to create the worker nodes.
1. ### Important Considerations
1. Ensure that the cluster creation and fetching details are synchronized correctly. The .apply
method ensures the values are only processed after they are available.
2. Handle exceptions and errors appropriately, especially when dealing with asynchronous operations.
1. This approach ensures that your EKS cluster details are correctly fetched and embedded into the user data template, resolving the issues you've faced with the asynchronous nature of Pulumi.*********lively-crayon-44649
06/17/2024, 9:42 AMOutput[str]
which you want to process and use to build a userdata attribute. If so I think you should be able to use Output.apply
for this; something like:
userdata=command.stdout.apply(lambda o: f"userdata stuff {o}")
If you need to split the output etc. first you can do that too, e.g.:
def parse_output(o: str) -> tuple[str, str]:
# split o on newlines, extract cluster urls, etc.
return endpoint, ca
def make_userdata(o: str) -> str:
endpoint, ca = parse_output(o)
return f"userdata stuff {endpoint} more stuff {ca}"
...
userdata=command.stdout.apply(make_userdata)
limited-guitar-4572
06/17/2024, 12:55 PMTo get the value of an Output[T] as an Output[str] consider:
1. o.apply(lambda v: f"prefix{v}suffix")
Into the contents of the userdata string.
The issue is passing in the Outout<T>.apply(lamda v: str(v)) as one of the transformation dicts in the below function. Python's native replace still sees it as an Ouput or if wrapped as a Python str results in the error message being injected into the userdata .
def encoded_user_data(
cluster_name: str, enable_edr: bool, user_data: str, transformations: list[dict]
):
# Reads user data script and encodes it as base 64
with open(f"data/{user_data}") as f:
data = f.read()
# Set enable EDR value in cloud init script
if enable_edr:
data = data.replace("<ENABLE_EDR>", "1")
else:
data = data.replace("<ENABLE_EDR>", "0")
# Replace use case specific fields or values
for t in transformations:
data = data.replace(t.get("key"), t.get("value"))
return base64.b64encode(data.encode()).decode("utf-8")
lively-crayon-44649
06/17/2024, 1:01 PMencoded_user_data
?limited-guitar-4572
06/17/2024, 3:30 PMnode_template = aws.ec2.LaunchTemplate(
f"{cluster_name}-node-lt",
image_id=launch_template_ami,
instance_type=instance_type,
block_device_mappings=[
aws.ec2.LaunchTemplateBlockDeviceMappingArgs(
device_name="/dev/xvda",
ebs=aws.ec2.LaunchTemplateBlockDeviceMappingEbsArgs(
volume_size=20, **ebs_args
),
),
aws.ec2.LaunchTemplateBlockDeviceMappingArgs(
device_name="/dev/sda1",
ebs=aws.ec2.LaunchTemplateBlockDeviceMappingEbsArgs(
volume_size=25, **ebs_args
),
),
aws.ec2.LaunchTemplateBlockDeviceMappingArgs(
device_name="/dev/sdb",
ebs=aws.ec2.LaunchTemplateBlockDeviceMappingEbsArgs(
volume_size=250, **ebs_args
),
),
],
key_name="packer_access",
user_data=encoded_user_data(
cluster_name,
enable_edr,
"user-data-eks",
[
{"key": "<CLUSTER>", "value": cluster_name},
{"key": "<CIDR>", "value": eks_cidr_block},
{"key": "<KUBE_API>", "value": kube_api},
{"key": "<CERTIFICATE_AUTHORITY>", "value": certificate_authority}
],
),
vpc_security_group_ids=[sg_id],
tags={"<http://karpenter.sh/discovery|karpenter.sh/discovery>": cluster_name},
)
lively-crayon-44649
06/17/2024, 3:32 PMcertificate_authority
, kupe_api
, etc.?limited-guitar-4572
06/17/2024, 3:32 PMlively-crayon-44649
06/17/2024, 3:32 PMapply
on those, which you can supply a callback that will be allowed access to the values "inside" the outputslively-crayon-44649
06/17/2024, 3:33 PMOutput[str]
, you can provide a function f(s: str) -> A
and get back an Output[A]
lively-crayon-44649
06/17/2024, 3:33 PMuser_data=Output.all(
cluster_name=cluster_name,
eks_cidr_block=eks_cidr_block,
kube_api=kube_api,
certificate_authority=certificate_authority,
).apply(lambda args: encoded_user_data(args["cluster_name"], enable_edr, "user-data-eks", [{"key": "<CLUSTER">, "value": args["cluster_name"]}, ...]))
(See https://www.pulumi.com/docs/concepts/inputs-outputs/all/ for more on all
)limited-guitar-4572
06/17/2024, 3:34 PM# Required to bootstrap Amazon Linux 2023 nodes
cmd_refresh_config = cmd.local.Command(
f"{cluster_config.name}-cmd-refresh",
create=f"aws eks update-kubeconfig --region us-east-1 --name {cluster_config.name}",
opts=pulumi.ResourceOptions(depends_on=[cluster])
)
cmd_get_cluster_cert_authority = cmd.local.Command(
f"{cluster_config.name}-cmd-get-cluster-cert-authority",
create=f"aws eks describe-cluster --name {cluster_config.name} --query cluster.certificateAuthority.data --output text --no-cli-pager",
opts=pulumi.ResourceOptions(depends_on=[cmd_refresh_config])
)
cmd_get_cluster_endpoint = cmd.local.Command(
f"{cluster_config.name}-cmd-get-cluster-endpoint",
create=f"aws eks describe-cluster --name {cluster_config.name} --query cluster.endpoint --output text --no-cli-pager",
opts=pulumi.ResourceOptions(depends_on=[cmd_refresh_config])
)
cluster_config.certificate_authority = cmd_get_cluster_cert_authority.stdout.apply(lambda v: str(v))
cluster_config.endpoint = cmd_get_cluster_endpoint.stdout.apply(lambda v: str(v))
lively-crayon-44649
06/17/2024, 3:38 PMoa: Output[A]
, ob: Output[B]
, oc: Output[C]
, ...
• A function f(a: A, b: B, c: C, ...) -> Result
You can use Output.all(oa, ob, oc, ...).apply(f)
to get an Output[Result]
If you have just the one Output
, o: Output[X]
and a function g(x: X) -> Y
then o.apply(g)
gives you Output[Y]
, and you can see this as the "degenerate" case of all
where you only have one argumentlimited-guitar-4572
06/17/2024, 3:41 PMlively-crayon-44649
06/17/2024, 3:42 PMapply
(or all().apply
)lively-crayon-44649
06/17/2024, 3:42 PMlimited-guitar-4572
06/17/2024, 3:44 PMlively-crayon-44649
06/17/2024, 3:48 PMlimited-guitar-4572
06/17/2024, 6:16 PM