#terraform

1 messages · Page 1 of 1 (latest)

pliant pollen
#

@hushed tinsel 👋

hushed tinsel
#

beautiful!

posting a snippet now

hushed tinsel
#

A simple example on how to run terraform actions with dagger 🧵

maiden crypt
signal ore
#

I used to have an approach working using local state files, but I switched to a cloud backend for security purposes. The plus side is it meant I could strip out all the code to load the state into/out of a buildkit cache 😅

#

(As a side note, I love the with_ API, in Python land it almost maps onto a decorator...)

maiden crypt
pliant pollen
#

I think they meant "the API of with_"

signal ore
#

Yep, exactly that, using the with_ method to layer custom things into the image and keep the function calls chainable

pliant pollen
#

Fun fact, all those functions are generated for all SDKs from the same core GraphQL schema 🙂

normal elbow
#

I am new to dagger and getting a little stumped with executing commands. Ideally my goal is to get the dagger pipeline to build an image with all my necessary tools - terraform, vault, AWS CLI, azure CLI, etc. pull/or copy files from git and run any terraform command that I pass into the pipeline.

My idea is that if I can get this python code setup I will be able to leverage it for local testing and then do a POC on GitHub actions or Jenkins for management and will always have this reusable IaC pipeline. If anyone has a working example of this I would love to look it over an dissect it. I just can’t seem to build something like this on my own from the current dagger docs. I get stumped when running any commands

tired solstice
#

To try it, I'd install the dagger cli
https://docs.dagger.io/cli/465058/install
and then:

git clone https://github.com/dagger/dagger && cd dagger/examples/sdk/python/db-service

python3 -m venv .venv && source .venv/bin/activate

pip install dagger-io

dagger run python pipeline.py
normal elbow
tired solstice
#

Nice! Looks like a nice set of functions!

normal elbow
#

I’m not a docker wiz, but are you able to merge multiple images to one container? Like the hashicorp terraform image with the vault image?

tired solstice
#

You could call those from a "main" and use some more of the Dagger API to publish the created image, for example 🙂

tired solstice
#

As well as build images for different machine architectures and make a final multi-arch image.

normal elbow
tired solstice
#

this is pretty nice 👆 it shows grabbing a binary file built in the build container and putting it in place in the prod_image.

#

Then publishing the resulting image.

#

Nothing gets executed until that publish() as the evaluation is lazy 🙂

normal elbow
#

So these examples are just building an image right and publishing it to docker?

tired solstice
#

You can grab whole directories/filesystems and copy them over (not just single files) if that helps.

#

Yep, that last example will build an image an publish it to ttl.sh, which is like pastebin + dockerhub 😉

#

dockerhub that lasts for an hour, essentially

normal elbow
#

Man that’s confusing to me lol, I view that code as two different containers, I don’t see how they’re merging two different docker images

tired solstice
#

but you could publish to any registry. Dockerhub, quay.io, ghcr, ecr, gcr wherever

normal elbow
tired solstice
#

You're right, they are two different containers, it's just that we only take one file out of one /src/dagger when we're done with it and put it in another one that we finally publish as an image. The first one is only a memory (and also in the cache so it's faster to do next time 😉 )

normal elbow
#

Ah so what I originally meant is like using two from statements for one container. So leveraging hashicorps terraform image with alpines image for one container to then work off of

tired solstice
#

right. each container image has two parts: config metadata and a rootfs filesystem. It wouldn't be clear how to merge two containers without sorting out which bits of config or filesystem to keep or drop or overwrite.

#

So in practice, we grab a container().from("a") and work with it to get something from it, then put that inside of container().from("b")

#

You could use a, b, c, d, e containers and assemble parts of them into a final f container

normal elbow
#

So what would make the most sense for me? I need an image that has various tools ontop of terraform like vault, AWS CLI, etc. would getting alpine and just stringing a bunch of different install commands be best or is it better practice to pull from the official images of each tool?

tired solstice
#

I think that's a really good/interesting question.

#

Looks like you can just download binaries for vault, terraform. It probably depends how what base image your need, and how the tools you need are installed. If they are single-file static binaries, then you can just download them from the vendor and drop them in, but if they need a bunch of config files and such to run that get dropped in place by apt/apk/yum or the vendor's curl | bash installer, then I'd definitely do that.

Looks like some of the cloud CLIs have requirements of their own (see Azure cli below).

So it's kind of a puzzle depending on whether you want the kitchen sink or the leanest image possible, which OS you need the final image to be, etc.

#

Ultimately though, I'd prob just find a base image that would work for all the tools, then I'd have little pipelines that assumed that base OS and did the installs by doing a with_exec to curl something or apt install something, etc.
Then you could compose those to get the sorts of things you need.

In the end, you may never need to have a container with everything installed that you put somewhere. Instead you can use one container to build the code, another to lint, another to deploy, and another to tf apply your Terraform.

Here's another nice example that can run Terrform you have in the local app directory: https://github.com/kpenfound/greetings-api/blob/main/ci/tasks/tf.go

normal elbow
#

Yeah that makes sense, I might just have to go alpine and then maintain a versioned image so there isn’t the build overhead each run of the pipeline

tired solstice
#

If you use Dagger for the cache, it will only have that overhead the first time 🙂

wanton mirage
#

As the author of the hashicorp apt repos, ill say they might be a pain to set up in a pipeline 😅 ill write up a snippet when im back at a computer

pliant pollen
normal elbow
hushed tinsel
#

@normal elbow to continue with @pliant pollen point, there is no need for base ci-tools images anymore because you can just grap whatever you need at runtime, Dagger will cache it all during the first run.

An example would be something like

nodeImage -> install and build my stuff
curlImage -> download and install some stuff from the internet
terraformImage -> init, plan, apply
...

The top message in this channel has a snippet to build some nodejs stuff and then when needed, just grab the tf image and run your thing
https://discord.com/channels/707636530424053791/1126718293290537011

normal elbow
# signal ore Glad it could be helpful!

Can you explain the plugin cache function? Are you creating a local volume and running terraform init on it then just pulling out the .terraform directory and setting it as the TF_PLUGIN_CACHE_ID?

signal ore
#

TF Cache

normal elbow
#

Huge thanks to @signal ore for their continued help on here. I got to a working point leveraging your code and picking it apart helped me greatly. I think I got to a working point with some tweaks to simplify it just to understand my working state. Currently it’s running against some dummy terraform code https://github.com/sblack4/terraform-terraform-hello-world.git so I don’t have to worry about the AWS creds for now lol. I just had to trim the fat. But this is exactly what I was looking for, now I can call this in an automated fashion, but still have the ability to pass in any terraform command with an —force flag. I’ll send a follow up message with example run commands, please let me know if there are any improvements I can make. I am not a python guru, just finessed it to get my idea to work lol. I have it running against one tf version now, but will run it against multiple in the future as a test case.

GitHub

testing pre-commit hooks and github actions. Contribute to sblack4/terraform-terraform-hello-world development by creating an account on GitHub.

normal elbow
normal elbow
#

Any ideas how I can limit terraform apply to ONLY run on a merge to master, yet still allow local users to run dagger?

Basically I want the CI/CD platform to trigger dagger after a PR is merged to master, but I want users to be able to run the dagger script locally and build their plans without all the overhead of constantly committing test code.

I’m not sure what type of logic in python would allow that. Interested to hear anyone’s thoughts of how to tackle it!

normal elbow
signal perch
pliant pollen
normal elbow
#

The apply will be done via a service account

final thistle
#

Hey guys, I'm struggling with a bug in a set of Dagger tasks that I'm writing in Go.
Basically, I'm fetching a private Git repository with a certain Terragrunt-based root-modules; this part works well. When I'm running a command to generate the output of the plan (here's the scenario, file generation), let's say WithExec([]string{"sh", "-c", "terragrunt plan -out=output.tfplan"}). it doesn't fail, but it does not generate the expected output.tfplan in the container. I'm even inspecting the Entries and printing the standard output, and nope! no file at all.
What I'm doing wrong?

// Generate the terraform plan file.
_, _ = tg.
    WithUnixSocket(sshSocketPath, client.Host().UnixSocket(sshSocketPath)).
    WithEnvVariable("SSH_AUTH_SOCK", sshSocketPath).
    WithEnvVariable("GIT_SSH_COMMAND", "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=accept-new").
    WithExec([]string{"sh", "-c", "terragrunt plan -out=output.tfplan"}).
    Stdout(ctx)
// Export the plan output.
planOutput, planErr := tg.
    WithUnixSocket(sshSocketPath, client.Host().UnixSocket(sshSocketPath)).
    WithEnvVariable("SSH_AUTH_SOCK", sshSocketPath).
    WithEnvVariable("GIT_SSH_COMMAND", "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=accept-new").
    WithExec([]string{"sh", "-c", "terragrunt show -json output.tfplan > output.json"}).
    Stdout(ctx)
if planErr != nil {
    return fmt.Errorf("failed to export plan output: %w", planErr)
}

// Inspect the generated plan output.
output, catErr := tg.
    WithEntrypoint(nil).
    WithEnvVariable("CACHEBUSTER", time.Now().String()).
    WithExec([]string{"sh", "-c", "cat output.json"}).
    Stdout(ctx)
if catErr != nil {
    return fmt.Errorf("failed to read plan output: %w", catErr)
}
bitter drift
#

@final thistle do you have any entrypoint for the docker image? .WithoutEntrypoint() helps in this case, also did it cache the WithExec command? in this case you can also use CACHEBUSTER to break the cache for the terragrunt plan

#

meanwhile im also struggling to understand the Dagger Terraform best practices and trying to cache all the provider/module folders. I could not find the best answer yet. let me know if you get some answers :)

wanton mirage
#

Hey guys, I'm struggling with a bug in a

hidden nexus
#

Heya folks - any ideas why I'd experience my terraform plugin cache dir not behaving as expected?

I've configured it like this:

async with dagger.Connection(config) as client:
    terraform = (
        client.container()
        .from_("hashicorp/terraform:1.7")
        .with_directory(
            "/terraform",
            client.host().directory(".", exclude=["./**/.terraform/", "ci/"]),
        )
        .with_env_variable("TF_PLUGIN_CACHE_DIR", plugin_cache_dir)
        .with_env_variable("TF_LOG", "INFO")
        .with_workdir("/terraform")
        .with_mounted_cache(plugin_cache_dir, client.cache_volume("terraform"))
    )

In the output I see lines like this:

 2024-02-11T20:27:06.114Z [WARN]  local provider path "/terraform/.terraform/providers/registry.terraform.io/hashicorp/random/3.6.0" contains invalid
┃  namespace "registry.terraform.io"; ignoring                                                                                                        
┃ 2024-02-11T20:27:06.114Z [WARN]  local provider path "/terraform/.terraform/providers/registry.terraform.io/hashicorp/random/3.6.0/linux_arm64" cont
┃ ains invalid namespace "registry.terraform.io"; ignoring                                                                                            
┃ 2024-02-11T20:27:06.114Z [WARN]  local provider path "/terraform/.terraform/providers/registry.terraform.io/hashicorp/random/3.6.0/linux_arm64/terra
┃ form-provider-random_v3.6.0_x5" contains invalid namespace "registry.terraform.io"; ignoring
hidden nexus
cloud zealot
cloud zealot
#

I can help you to get unblocked

hidden nexus
cloud zealot
bronze atlas
#

or tf plugin cache dir is fucked (env path is not absolute or somehow misleading)

hidden nexus
#

OK I had a go at this dagger functions stuff from the latest release and I'm a fan - I wrote some dagger function to terraform fmt check a bunch of terraform in my project, how is my dagger? Anything you'd change? I'd appreciate any feedback before I wrote a whole bunch more:

import os

import dagger
from dagger import dag, function, object_type

TERRAFORM_VERSION = "1.7.4"


@object_type
class Terraform:
    @property
    def container(self) -> dagger.Container:
        return dag.container().from_(f"hashicorp/terraform:{TERRAFORM_VERSION}")

    @staticmethod
    async def get_terraform_dirs_recursively(directory: dagger.Directory) -> set[str]:
        return set(
            filter(
                lambda dir: ".terraform" not in dir
                and "nextflow-tower" not in dir
                and "config" not in dir,
                map(
                    os.path.dirname,
                    await directory.glob("**/*.tf")
                    + await directory.glob("**/*.tfvars"),
                ),
            )
        )

    @function
    async def check(self, source: dagger.Directory) -> str:
        container = self.container.with_mounted_directory("/mnt", source)

        return "".join(
            [
                await container.with_workdir(f"/mnt/{dir}")
                .with_exec(["fmt", "-check"])
                .stdout()
                for dir in await self.get_terraform_dirs_recursively(source)
            ]
        )
#

Maybe I should do the filtering at the point of mounting the directory rather than after the fact?

#

Yes (to answer my own question):

import dagger
from dagger import dag, function, object_type

TERRAFORM_VERSION = "1.7.4"


@object_type
class Terraform:
    @property
    def container(self) -> dagger.Container:
        return dag.container().from_(f"hashicorp/terraform:{TERRAFORM_VERSION}")

    @function
    def check(self, source: dagger.Directory) -> dagger.Container:
        container = self.container.with_mounted_directory(
            "/mnt",
            source.without_directory("config/").without_directory("nextflow-tower/"),
        ).with_workdir("/mnt")

        return container.with_exec(["fmt", "-check", "-recursive"])
normal elbow
#

@hidden nexus I want to redo my pipeline to fit the new module approach, but I have quite a bit going on. My whole idea was to be able to have this copy and pasted into any CI platform and then I can run native terraform commands through some Pull Request Automation. I want the ability to have terraform, terragrunt, vault, etc so I tried to make it where I can add those tools is. I’m not a Python expert tho haha how do you think I can best modularize this?

https://pastebin.com/pUt7JD5S

quasi tartan
#

I think some examples of terraform in the dagger repo would really help. especially with tf state, init, plan, apply.

#

I saw a Pulumi example in there, and terraform is similar to that

quasi tartan
#

would they "get it" enough to understand

main flume
# quasi tartan https://gist.github.com/pjmagee/88472a36adb8a58ccdd45e55f9b54123 Does this make...

Its a good starting point! In my experience the main thing that gets complicated with IaC repositories is not so much the execution of the CLI itself, rather the ordering of the applies and how dependencies between the different states happen. That is where dagger IMO really shines, when your CI pipeline requires you to program logic and ordering in a specific way. I'm happy to join dev-audio and discuss a bit more what your setup looks like and how/if dagger is a good fit for that!

As for the specific example, a few notes (mostly my opinion):

  • When wrapping CLIs, I like having top level functions for relevant subcommands while also providing flexibility with an Exec function. So that users can do: dag.Terraform.Exec("command with custom flags")
  • Making available the underlying container where the CLI runs is often a must. Tools sometimes have files, credentials or other special stuff that if you do not support it via a parameter, it makes it a blocker for using the module. Exposing the container gives users some flexibility to manipulate it
  • I like the approach of returning Container, error like you did. It gives the most flexibility, if the module is being used from the cli you can always do dagger call <method> stdout to get the output.

I don't have a terraform specific example, but like you said, IaC tools are all very similar. I wrote a blog post last year about building a CI/CD pipeline for an IaC repository. Hope it helps: https://blog.matiaspan.dev/posts/exploring-dagger-building-a-ci-cd-pipeline-for-iac/

quasi tartan
#

@main flume Thank you for this feedback, it's really useful. I submitted an RFC in the company and hoping I can demonstrate a simple example, I'll also be referencing external material such as blogs, so this is really great

autumn flicker
#

Hey folks! Here's my first stab at a daggerized deployment pipeline for Terramate using OpenTofu: https://gist.github.com/digorgonzola/a3f279890740cb19f8613146971e1665

Let me know your thoughts... This is pretty rough and I'd be keen to know how it could be improved.

I'm still very raw in my understanding of how Dagger works and it's possible that my way of passing around container objects between functions could be improved a lot!

Gist

Dagger functions for Terramate. GitHub Gist: instantly share code, notes, and snippets.

pliant pollen
autumn flicker
autumn flicker
# pliant pollen Hello! This looks like a solid start! A few suggestions: - That inline Dockerfi...

Struggling a bit with simplifying the function args. I'm guessing I can do something like this where I declare an __init__ function that sets some defaults:

    def __init__(self):
        """
        Initialize the module with defaults or environment variable overrides.
        """
        self.aws_dir = "$HOME/.aws"
        self.terramate_dir = "$HOME/.terramate.d"
        self.aws_profile = os.environ.get("AWS_PROFILE")
        self.source = os.getenv("$PWD")

But the .with_directory method expects a type dagger.Directory...

wanton mirage
# autumn flicker Struggling a bit with simplifying the function args. I'm guessing I can do somet...

Have you seen the docs on setting defaults? https://docs.dagger.io/api/arguments/#default-values
It applies to constructors too!

Dagger Functions, just like regular functions, can accept arguments. In addition to basic types (string, boolean, integer, arrays...), Dagger also defines powerful core types which Dagger Functions can use for their arguments, such as Directory, Container, Service, Secret, and many more.

autumn flicker
prisma quarry
#

how could Dagger be used to build pipelines for things like this https://aws-workshop.terraconstructs.dev/20-typescript/30-hello-cdk/200-lambda.html#add-an-aws-lambda-function-to-your-stack

basically, a tighter coupling to the TF state management aspect? Using TS/Golang/Python to define TF Config (needs to run synth), but automating the terraform state dependencies, is that something dagger could help with?

#

feels like there's a need to build some type of "glue" CLI that handles the synth step and keeps track of other terraform states to track dependencies and Dagger would just be the orchestrator?