<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[There and back again]]></title><description><![CDATA[A former co-founder of Hotjar writing about his next adventure - building a Company of One from scratch.]]></description><link>https://blog.ekik.org</link><generator>RSS for Node</generator><lastBuildDate>Wed, 22 Apr 2026 20:04:21 GMT</lastBuildDate><atom:link href="https://blog.ekik.org/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[My experience migrating my infrastructure from Terraform to Pulumi]]></title><description><![CDATA[I always intended this blog to contain a mix of technical and business posts. Here's the first technical piece. If that's not your cup of tea then you should probably stop reading right here, and go for a nice walk outside instead :).
Still here? Coo...]]></description><link>https://blog.ekik.org/my-experience-migrating-my-infrastructure-from-terraform-to-pulumi</link><guid isPermaLink="true">https://blog.ekik.org/my-experience-migrating-my-infrastructure-from-terraform-to-pulumi</guid><category><![CDATA[infrastructure]]></category><category><![CDATA[Terraform]]></category><dc:creator><![CDATA[Erik Näslund]]></dc:creator><pubDate>Wed, 26 May 2021 16:13:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1620814893689/6jmissHNl.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I always intended this blog to contain a mix of technical and business posts. Here's the first technical piece. If that's not your cup of tea then you should probably stop reading right here, and go for a nice walk outside instead :).</p>
<p>Still here? Cool! Today we're talking about <a target="_blank" href="https://en.wikipedia.org/wiki/Infrastructure_as_code">IaC</a> (Infrastructure as Code), and a relative newcomer on the scene called  <a target="_blank" href="https://www.pulumi.com">Pulumi</a> .</p>
<p>I've been using  <a target="_blank" href="https://www.terraform.io">Terraform</a> for a couple of years and overall I've been quite happy with it. However there's a few things that started to bother me more and more recently.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1621421844790/E57CCF6a9.png" alt="image.png" /></p>
<p>Terraform uses a language called Hashicorp HCL to define the infrastructure. It's a relatively simple declarative language, but it's something I had to learn along the way. Just like any language it has it's little quirks, and I often found myself spending more time than I wanted to figure out how to do certain things. As I'm doing all the infrastructure myself <strong>I really wanted to be able to use a language I'm familiar with, to make things simple</strong>.</p>
<p>I had written my own <a target="_blank" href="https://www.terraform.io/docs/language/modules/develop/index.html">Terraform modules</a> to organize my code. Modules are a great help when it comes to organizing your code in a logical way, and I strongly recommend it if you haven't tried using them already. However, after using it for a while I reliazed that the <strong>Terraform <a target="_blank" href="https://www.terraform.io/docs/language/expressions/types.html">type system</a> left something to be desired</strong>. There are a few basic types available, but not much more than that. This caused bad "micro interactions" between me and Terraform. It was commonplace for me to submit an MR to my Gitlab CI where I passed in a security group <em>name</em> instead of an <em>arn</em> or <em>id</em>.</p>
<p>The third and perhaps most important point is that I think <strong>a custom language like HCL is the wrong way to go long term</strong>. Don't get me wrong - I love the people at Hashicorp and I'm forever grateful for Terraform because it's the first product that made me enjoy writing IaC. That being said, I think it's a better choice to write libraries for existing languages, instead of trying to create a configuration language of your own. There's a lot of constructs that I'm used to having in Python that I sorely miss having available in Terraform. In my opinion there's no reason to develop your own configuration language since it's a time consuming task, and I don't find it better than a general purpose language anyways.</p>
<h3 id="why-did-i-choose-pulumi">Why did I choose Pulumi?</h3>
<p>I have this thing called "Fun time Friday" where I spend the first half day on Fridays exploring new things I've heard about. Most of the things I look at during that time end up being scrapped, and there's zero pressure to push things into production.</p>
<p>However after exploring Pulumi I felt that it might be significantly better for me than Terraform. I decided to take a day to see if I could migrate my infrastructure.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1621421875480/3yr01Im2O.png" alt="image.png" /></p>
<p>Could I have used something else, like  <a target="_blank" href="https://aws.amazon.com/cdk/">AWS CDK</a>? I most probably could. The nice thing about running a company of one is that there's no need for analysis paralysis! At this point I believed that Pulumi was a better choice for me than Terraform, so I went for it.</p>
<p>Personally I struggle a bit with CDK being mostly Amazon-centric. I sometimes use my IaC to configure things outside of AWS as well. The other issue for me is that CDK by default uses AWS CloudFormation to provision resources. I've been bitten a few times by CF. Some times it's been because of buggy behavior, e.g. a stack that refused to terminate. Other times it's been the fact that CF often lags behind in capabilities, so it's common to have to wait a few months for a new feature to be usable through CF.</p>
<p>Sure, I could use  <a target="_blank" href="https://www.hashicorp.com/blog/cdk-for-terraform-enabling-python-and-typescript-support">CDK for Terraform</a> to have CDK genererate Terraform HCL for me. However, now we're quickly getting away from the <strong>simple</strong> system I wanted in the first place.</p>
<p>Apologies to those of you who are screaming <em>"FFS - CDK is the best thing ever!"</em> at your monitors right now. You might be 100% right! From what I've seen CDK seems pretty awesome for the most part. I didn't end up with Pulumi because CDK seemed bad. I ended up with Pulumi because it seemed like a slightly better fit for my needs :).</p>
<h3 id="pulumi-in-3-minutes">Pulumi in 3 minutes</h3>
<p>You might already know a bit of Pulumi, but here's a very quick overview plus some advice on where to go from here.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1621420036888/_IkCVbyKu.png" alt="image.png" /></p>
<ul>
<li>Pulumi allows you to write your IaC using TypeScript, Javascript, Python, Go or C#.</li>
<li>You define <em>resources</em> which have <em>inputs</em> and <em>outputs</em>. As an example, a resource could be an RDS database Instance. One of it's inputs would be the database version to use. One of the outputs would be the hostname / address of the server.</li>
<li>The outputs are lazily evaluated, and their exact value is often not known until after you've executed your code. I.e. you wouldn't have the <code>db_instance.address</code> until the instance had been created. Think of outputs as promises / futures.</li>
<li>You can use  <a target="_blank" href="https://www.pulumi.com/docs/intro/concepts/resources/#components">component resources</a> to group your resources into logical groups - similar to how Terraform modules work. If you don't do this there's a high risk you'll end up with a very messy setup further down the line.</li>
<li>Read Pulumi's own  <a target="_blank" href="https://www.pulumi.com/docs/intro/concepts/">overview</a>  and  <a target="_blank" href="https://www.pulumi.com/docs/intro/concepts/resources/#autonaming">naming</a> docs before attempting to use Pulumi yourself. I'd say that's the most important bit to know about.</li>
<li>Pulumi are using the  <a target="_blank" href="https://en.wikipedia.org/wiki/Open-core_model">open core</a> model where most features are free, but some extra goodies are on a paid tier. You can use the open source version and do state management yourself, but you miss out on certain features.</li>
</ul>
<p>So what does Pulumi code look like? Below is a minimal extract of some of the Python code I've written (slightly altered).</p>
<pre><code class="lang-python"><span class="hljs-comment"># __main__.py</span>
...
prod_redis_server = RedisServer(
    identifier=<span class="hljs-string">'redis-prod'</span>,
    route53_zone=example_com_route53_zone,
    route53_name=<span class="hljs-string">'redis-prod.example.com'</span>,
    security_groups=[my_ecs_service.task_security_group]
)
...

<span class="hljs-comment"># components/redis_server.py</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RedisServer</span>(<span class="hljs-params">ComponentResource</span>):</span>    
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">
        self,
        identifier: str,
        route53_zone: aws.route53.Zone,  <span class="hljs-comment"># Route53 Zome where the DNS record will be created.</span>
        route53_name: str,  <span class="hljs-comment"># Full DNS name of the CNAME record, e.g. 'redis.example.com'.</span>
        security_groups: List[aws.ec2.SecurityGroup],  <span class="hljs-comment"># Security groups which should be granted access to the server.</span>
        opts=None
    </span>):</span>    
        super().__init__(<span class="hljs-string">'vt:RedisServer'</span>, identifier, <span class="hljs-literal">None</span>, opts)

        <span class="hljs-comment"># Prefix with `self.` if you want to access the security group from outside this component.</span>
        security_group = aws.ec2.SecurityGroup(
            identifier,
            ingress=[
                aws.ec2.SecurityGroupIngressArgs(
                    from_port=<span class="hljs-number">6379</span>,
                    to_port=<span class="hljs-number">6379</span>,
                    protocol=<span class="hljs-string">'tcp'</span>,
                    security_groups=[sg.id <span class="hljs-keyword">for</span> sg <span class="hljs-keyword">in</span> security_groups],
                )
            ],
            opts=ResourceOptions(parent=self),
        )

        <span class="hljs-comment"># Create all the other sub resources needed.</span>
        ...
</code></pre>
<p>As you can see it's really simple and clean for me to set up a Redis server now.</p>
<p>There's also the fact that I can use the Pulumi Console (web interface) to check the state of my infrastructure really quickly. I found myself doing this instead of logging in to the AWS console or using the AWS cli.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1621421138885/aKhSOjrq_.png" alt="image.png" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1621421507994/clPpwtQd6.png" alt="image.png" /></p>
<h3 id="how-i-went-about-the-migration">How I went about the migration</h3>
<p>After reading the  <a target="_blank" href="https://www.pulumi.com/docs/guides/adopting/from_terraform/">migration guide</a> I immediately tried out what seemed to be the obvious option -  <a target="_blank" href="https://www.pulumi.com/tf2pulumi/">tf2pulumi</a>. This is supposed to be able to convert Terraform code directly to Pulumi. In my case it simply errored out and was not able to convert my Terraform code. To be fair my TF code was using slightly more "advanced" features like custom written modules, so it wasn't the easiest thing to handle for tf2pulumi. I recommend you try it out, because it sounds great in theory. However it wasn't an option for me, so I had to go for a more manual approach. In retrospect I'm actually quite happy I did, because it gave me the opportunity to organise my code in a way that I preferred.</p>
<p>Don't worry though - "manual" imports using Pulumi is still rather easy to perform, albeit a bit time consuming. Here's how I did it.</p>
<p>I started by testing out the  <a target="_blank" href="https://www.pulumi.com/docs/get-started/aws/">AWS getting started guide</a> to get a feel for how Pulumi worked. This was a great move and I recommend everyone else to do the same.</p>
<p>I was then ready to start importing my existing resources. The general workflow was to look at my existing Terraform code, rewrite it using Pulumi, and then import the existing resources. It was a bit time consuming in the beginning, but I got a lot faster at it after a few hours.</p>
<p>Since Pulumi starts out "empty" you can rewrite and import a piece at a time. I'd recommend doing this because you will most probably do some mistakes initially, and it's good to get early feedback.</p>
<p>You can check the  <a target="_blank" href="https://www.pulumi.com/docs/reference/pkg/aws/s3/bucket/">API reference</a> for details on how to import each specific resource. The instructions on how to do it is usually at the bottom of the reference page for each resource type.</p>
<p>Once I had everything migrated to Pulumi I set up a Gitlab CI/CD flow which allowed me to preview and apply the infrastructure changes. Up until then I'd been doing it locally from my own computer. If possible, I recommend doing it that way. The extra time taken by involving a separate CI/CD pipeline would make your migration take a lot longer. It's perfectly fine when everything is done and dusted, however when learning Pulumi you'll often need to use the <code>pulumi</code> command to look at the resources.</p>
<h3 id="issues-workarounds-and-general-tips">Issues, workarounds and general tips</h3>
<h4 id="use-component-resources-to-group-resources">Use component resources to group resources</h4>
<p>When your infrastructure starts growing it becomes hard to manage unless you group your resources. Pulumi's solution to this are the so called  <a target="_blank" href="https://www.pulumi.com/docs/intro/concepts/resources/#components">component resources</a>. They are "logical components" which you define yourself.</p>
<pre><code>$ pulumi stack
<span class="hljs-meta">...</span>
─ vt:RedisServer                                                          vt-redis-prod
    │  ├─ aws:elasticache/parameterGroup:ParameterGroup                        vt-redis-prod
    │  ├─ aws:ec2/securityGroup:SecurityGroup                                  vt-redis-prod
    │  └─ aws:elasticache/cluster:Cluster                                      vt-redis-prod
<span class="hljs-meta">...</span>
</code></pre><p>As you can see I've defined a component called "vt:RedisServer" which I use to group all the resources associated with my Redis server. You'll get a graph representation of your whole infrastructure in the  <a target="_blank" href="https://www.pulumi.com/docs/intro/console/">Pulumi Web Console</a> .</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1622019946304/_aL1U18Lr.png" alt="image.png" /></p>
<p>It might seem like a small thing, but it <em>really</em> helps to have a logical grouping of your resources as your infrastructure grows. It also help because you can suddenly decide which resources your component exposes and which ones are "internal" to the component. Check out the code sample from earlier in the article to see how to do this using <code>self.</code>.</p>
<h4 id="importing-a-resource-that-has-a-parent-was-a-bit-confusing-at-first">Importing a resource that has a parent was a bit confusing at first</h4>
<p>Each resource in pulumi has a globally unique  <a target="_blank" href="https://www.pulumi.com/docs/intro/concepts/resources/#urns">URN</a>. You can show the URNs for the infrastructure you've codeified in pulumi by issuing <code>pulumi stack --show-urns</code></p>
<pre><code>$ pulumi stack --show-urns
...
├─ <span class="hljs-symbol">vt:</span>RedisServer                                                          vt-redis-prod
    │  │  <span class="hljs-symbol">URN:</span> <span class="hljs-symbol">urn:</span><span class="hljs-symbol">pulumi:</span>prod::VT::<span class="hljs-symbol">vt:</span>RedisServer::vt-redis-prod
    │  ├─ <span class="hljs-symbol">aws:</span>elasticache/<span class="hljs-symbol">parameterGroup:</span>ParameterGroup                        vt-redis-prod
    │  │     <span class="hljs-symbol">URN:</span> <span class="hljs-symbol">urn:</span><span class="hljs-symbol">pulumi:</span>prod::VT::<span class="hljs-symbol">vt:</span>RedisServer$aws<span class="hljs-symbol">:elasticache/parameterGroup</span><span class="hljs-symbol">:ParameterGroup</span><span class="hljs-symbol">:</span><span class="hljs-symbol">:vt-redis-prod</span>
    │  ├─ <span class="hljs-symbol">aws:</span>ec2/<span class="hljs-symbol">securityGroup:</span>SecurityGroup                                  vt-redis-prod
    │  │     <span class="hljs-symbol">URN:</span> <span class="hljs-symbol">urn:</span><span class="hljs-symbol">pulumi:</span>prod::VT::<span class="hljs-symbol">vt:</span>RedisServer$aws<span class="hljs-symbol">:ec2/securityGroup</span><span class="hljs-symbol">:SecurityGroup</span><span class="hljs-symbol">:</span><span class="hljs-symbol">:vt-redis-prod</span>
    │  └─ <span class="hljs-symbol">aws:</span>elasticache/<span class="hljs-symbol">cluster:</span>Cluster                                      vt-redis-prod
    │        <span class="hljs-symbol">URN:</span> <span class="hljs-symbol">urn:</span><span class="hljs-symbol">pulumi:</span>prod::VT::<span class="hljs-symbol">vt:</span>RedisServer$aws<span class="hljs-symbol">:elasticache/cluster</span><span class="hljs-symbol">:Cluster</span><span class="hljs-symbol">:</span><span class="hljs-symbol">:vt-redis-prod</span>
...
</code></pre><p>Let's have a look at one of the URNs in more detail, shall we?</p>
<pre><code><span class="hljs-symbol">urn:</span><span class="hljs-symbol">pulumi:</span>prod::VT::<span class="hljs-symbol">vt:</span>RedisServer$aws<span class="hljs-symbol">:elasticache/parameterGroup</span><span class="hljs-symbol">:ParameterGroup</span><span class="hljs-symbol">:</span><span class="hljs-symbol">:vt-redis-prod</span>
</code></pre><ul>
<li><strong>stack name:</strong> prod</li>
<li><strong>project name:</strong> VT</li>
<li><strong>parent resource type:</strong> vt:RedisServer</li>
<li><strong>resource type:</strong> aws:elasticache/parameterGroup:ParameterGroup</li>
<li><strong>resource name:</strong> vt-redis-prod</li>
</ul>
<p><strong>All URNs need to be unique, which means every resource of the same type, with the same parent (in the same stack and project) needs to have a unique resource name</strong>.</p>
<p>Normally I would have written and imported my parameter group definition something like this:</p>
<pre><code class="lang-python"><span class="hljs-comment"># components/redis_server.py</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RedisServer</span>(<span class="hljs-params">ComponentResource</span>):</span>
    <span class="hljs-string">"""
    Redis server running on ElastiCache (single node).
    """</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, identifier: str, opts=None</span>):</span>
        super().__init__(<span class="hljs-string">'vt:RedisServer'</span>, identifier, <span class="hljs-literal">None</span>, opts)

        ...

       parameter_group = aws.elasticache.ParameterGroup(
            identifier,
            name=identifier,
            family=<span class="hljs-string">'redis6.x'</span>,
            opts=ResourceOptions(parent=self),
        )

<span class="hljs-comment"># __main__.py</span>
vt_prod_redis_server = RedisServer(
    identifier=<span class="hljs-string">'vt-redis-prod'</span>
)
</code></pre>
<p>And I would then try to import it like this:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Executing the import (THIS WON'T WORK, DON'T TRY IT)</span>
pulumi import \
    aws:elasticache/parameterGroup:ParameterGroup \
    vt-redis-prod \
    vt-redis-prod
</code></pre>
<p>The first <code>vt-redis-prod</code> in the command above is the <em>identifier</em> used by the <code>aws.elasticache.ParameterGroup</code>. This is actually called "resource name" using Pulumi-speak. I prefer to call it identifier in my own code though, because "resource name" is too similar to the actual name it has to AWS for my liking.</p>
<p>The second <code>vt-redis-prod</code> is the actual name of my parameter group on AWS (remember we're importing existing stuff).</p>
<p>However, the above won't work for one simple reason - the URN won't match because we haven't told Pulumi that this parameter group resource we're importing is a child of our <code>RedisServer</code> component. What's the solution? Pretty simple, but somewhat obscure at the same time:</p>
<pre><code><span class="hljs-comment"># Executing the import (the right way)</span>
pulumi import \
    --<span class="hljs-built_in">parent</span> vt_prod_redis_server=urn:pulumi:prod::VT::vt:RedisServer::vt-redis-prod \
    aws:elasticache/parameterGroup:ParameterGroup \
    vt-redis-prod \
    vt-redis-prod
</code></pre><p>You tell Pulumi about the parent by using the <code>--parent</code> argument (this is the simple part). The <em>parent</em> argument is a key=value pair, and the key is actually the <em>python variable name</em> you've used in your code. If you scroll back up you can see that I stored my RedisServer instance in a variable called <code>vt_prod_redis_server</code>. This part took longer than I'd like to admit for me to figure out.</p>
<p>The value is the URN of the parent component, which you can get with <code>pulumi stack --show-urns</code>.</p>
<h4 id="you-cant-import-a-child-without-having-the-parent-in-place-first">You can't import a child without having the parent in place first</h4>
<p>That last one was a bit heavy, let's take a breather and go for something a bit simpler!</p>
<p>When importing a child component you need to import the parent first. If you simply run <code>pulumi up</code> it will try to import all your resources all at once, and it won't work because the parent haven't been created yet.</p>
<p>Instead simply do this first to update just a single resource:</p>
<pre><code><span class="hljs-selector-tag">pulumi</span> <span class="hljs-selector-tag">up</span> <span class="hljs-selector-tag">--target</span> <span class="hljs-selector-tag">urn</span><span class="hljs-selector-pseudo">:pulumi</span><span class="hljs-selector-pseudo">:prod</span><span class="hljs-selector-pseudo">::VT</span><span class="hljs-selector-pseudo">::vt</span><span class="hljs-selector-pseudo">:RedisServer</span><span class="hljs-selector-pseudo">::vt-redis-prod</span>
</code></pre><p>This will cause my <em>vt-redis-prod</em> resource to be created in the Pulumi state. Nothing actually happens on AWS since it's just a logical component. Once this is done you'll be able to import all of it's child resources as you saw above.</p>
<h1 id="writing-iam-json-policies-with-pulumi-outputs">Writing IAM (JSON) policies with Pulumi outputs</h1>
<p>Imagine having an IAM policy that referenced an ECR repository, just as an example. You might want the policy to grant access only to that particular ECR repository. You might be temped to try something like this:</p>
<pre><code class="lang-python">aws.iam.Policy(
    ...
    policy=json.dumps({
        <span class="hljs-string">'Version'</span>: <span class="hljs-string">'2012-10-17'</span>,
        <span class="hljs-string">'Statement'</span>: [
            {
                <span class="hljs-string">'Sid'</span>: <span class="hljs-string">'1'</span>,
                <span class="hljs-string">'Effect'</span>: <span class="hljs-string">'Allow'</span>,
                <span class="hljs-string">'Action'</span>: [
                    <span class="hljs-string">'ecr:GetAuthorizationToken'</span>,
                    ...
                ],
                <span class="hljs-string">'Resource'</span>: ecr_repository.arn
            }
        ]
    }))
</code></pre>
<p>While this might look sensible, it won't quite work. Remember early on I sad that Pulumi outputs are <em>futures</em>, and not variables whose values you can access just like that. If you did like the example above your policy would just end up with something similar to <code>'Resource': &lt;Pulumi.Output(...)&gt;</code> in it. Not quite what we wanted.</p>
<p>Instead we have to tell Pulumi to resolve these futures, so we get the actual <em>value</em> for them, like this:</p>
<pre><code class="lang-python">aws.iam.Policy(
    ...
    policy=ecr_repository.arn.apply(<span class="hljs-keyword">lambda</span> arn: json.dumps({
        <span class="hljs-string">'Version'</span>: <span class="hljs-string">'2012-10-17'</span>,
        <span class="hljs-string">'Statement'</span>: [
            {
                <span class="hljs-string">'Sid'</span>: <span class="hljs-string">'1'</span>,
                <span class="hljs-string">'Effect'</span>: <span class="hljs-string">'Allow'</span>,
                <span class="hljs-string">'Action'</span>: [
                    <span class="hljs-string">'ecr:GetAuthorizationToken'</span>,
                    ...
                ],
                <span class="hljs-string">'Resource'</span>: arn
            }
        ]
    }))
</code></pre>
<p>If you have more than one Output you need to access you can use <code>Output.all</code>, like this:</p>
<pre><code class="lang-python">policy=(
    Output.all(origin_access_identity.iam_arn, self.s3_bucket.arn)
    .apply(
        <span class="hljs-keyword">lambda</span> args: json.dumps({
            <span class="hljs-string">'Version'</span>: <span class="hljs-string">'2008-10-17'</span>,
            <span class="hljs-string">'Id'</span>: <span class="hljs-string">'PolicyForCloudFrontPrivateContent'</span>,
            <span class="hljs-string">'Statement'</span>: [
                {
                    <span class="hljs-string">'Sid'</span>: <span class="hljs-string">'1'</span>,
                    <span class="hljs-string">'Effect'</span>: <span class="hljs-string">'Allow'</span>,
                    <span class="hljs-string">'Principal'</span>: {
                        <span class="hljs-string">'AWS'</span>: args[<span class="hljs-number">0</span>]
                    },
                    <span class="hljs-string">'Action'</span>: <span class="hljs-string">'s3:GetObject'</span>,
                    <span class="hljs-string">'Resource'</span>: <span class="hljs-string">f'<span class="hljs-subst">{args[<span class="hljs-number">1</span>]}</span>/*'</span>
                }
            ]
        })
    )
)
</code></pre>
<p>The <code>.apply</code> method can also be useful for troubleshooting when you need to access the particular value of an output.</p>
<p>This might seem tricky at first, but digest this for a bit, then have a look at the docs for  <a target="_blank" href="https://www.pulumi.com/docs/intro/concepts/inputs-outputs/">Inputs and Outputs</a> and I'm sure it'll all make sense.</p>
<h4 id="dont-name-resources-unless-you-really-have-to">Don't name resources unless you really have to</h4>
<p>Pulumi is pretty smart when it comes to naming the actual resources on AWS. If you don't specify a <code>name</code> when creating a resource Pulumi will use the identifier and  <a target="_blank" href="https://www.pulumi.com/docs/troubleshooting/faq/#why-do-resource-names-have-random-hex-character-suffixes">append a hex character suffix</a> .</p>
<p>This gives two great advantages. The first is that you can have multiple stacks next to each other, e.g. a "review" and a "production" environment. If you hard coded the names this wouldn't be possible.</p>
<p>The second advantage is that it makes replacement of resources much easier. If you hard code the name you leave Pulumi no choice but to tear down the existing resource before creating the replacement one. This makes a big difference if you are to perform a version update of one of your dependencies. In case you allow Pulumi to handle the naming it can create the new resource before terminating the previous one. This coupled with a DNS entry update can make for updates with virtually zero downtime.</p>
<p>Since I imported existing resources I had to keep hard coded names in a few places. However I've taken the time to get rid of the hard coding as much as possible since then.</p>
<h4 id="integrating-with-cicd-gitlab">Integrating with CI/CD (Gitlab)</h4>
<p>I use Gitlab for my Ci/CD, and I'm really happy with it. Let me show you what my very simple Pulumi pipeline looks like right now:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">stages:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">build</span>

<span class="hljs-attr">preview:</span>
  <span class="hljs-attr">stage:</span> <span class="hljs-string">build</span>
  <span class="hljs-attr">image:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">pulumi/pulumi-python:3.2.1</span>  <span class="hljs-comment"># Choose the right image for your language</span>
    <span class="hljs-attr">entrypoint:</span> [<span class="hljs-string">""</span>]  <span class="hljs-comment"># This is important for things to work</span>
  <span class="hljs-attr">script:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">pulumi</span> <span class="hljs-string">stack</span> <span class="hljs-string">select</span> <span class="hljs-string">prod</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">pulumi</span> <span class="hljs-string">preview</span> <span class="hljs-string">--color</span> <span class="hljs-string">always</span> <span class="hljs-string">--diff</span>  <span class="hljs-comment"># I added '--diff' to always see more details</span>
  <span class="hljs-attr">only:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">merge_requests</span>  <span class="hljs-comment"># Only run the preview on merge requests</span>

<span class="hljs-attr">update:</span>
  <span class="hljs-attr">stage:</span> <span class="hljs-string">build</span>
  <span class="hljs-attr">image:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">pulumi/pulumi-python:3.2.1</span>
    <span class="hljs-attr">entrypoint:</span> [<span class="hljs-string">""</span>]
  <span class="hljs-attr">script:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">pulumi</span> <span class="hljs-string">stack</span> <span class="hljs-string">select</span> <span class="hljs-string">prod</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">pulumi</span> <span class="hljs-string">update</span> <span class="hljs-string">--color</span> <span class="hljs-string">always</span> <span class="hljs-string">--diff</span> <span class="hljs-string">--yes</span>  <span class="hljs-comment"># Answer "yes" when asking to apply the changes</span>
  <span class="hljs-attr">only:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">master</span>  <span class="hljs-comment"># Only run the update on the master branch (post merge)</span>
</code></pre>
<p>As you can see my pipeline is very simple for this little project which is still in an Alpha phase. There's just the "production" environment, and changes are automatically applied once the MR is merged.</p>
<p>You'll have to make use of your gitlab-fu (or other-ci-cd-fu) to adapt this to your own needs. Read the  <a target="_blank" href="https://www.pulumi.com/docs/guides/continuous-delivery/">docs from Pulumi</a> to get started. It's usually very simple and straight forward!</p>
<p>I did things quite differently from what's described in the Pulumi docs. I used the ready made image from Docker Hub, while they build their own. I did this to make the whole pipeline quicker. It seems to work really well so far, and I haven't found any downsides to my approach (yet).</p>
<h4 id="i-couldnt-get-the-gitlab-integration-webhooks-to-work">I couldn't get the Gitlab integration webhooks to work</h4>
<p>You are normally able to have Pulumi post a report of the planned changes  <a target="_blank" href="https://www.pulumi.com/docs/guides/continuous-delivery/gitlab-app/">attached to your merge request</a> .</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1622041677327/fCxWEQvQL.png" alt="image.png" /></p>
<p>However I couldn't make this work because of authentication issues. With the help of their support (which was excellent btw) we figured out that it was related to the fact that I was using  <a target="_blank" href="https://docs.gitlab.com/ee/user/group/">Gitlab groups</a> to organise my repositories.</p>
<p>As things stand right now you have to create a Pulumi Organization for each Gitlab Group. There's a bit of an issue with Pulumi's pricing right now, because the only way to be able to get more than 1 organisation is to go for their enterprise plan.</p>
<p>I think Pulumi will be fixing this in one way or anotherin the future. For now I'm still pretty happy just checking my changes from the Gitlab job output.</p>
<h3 id="whats-the-verdict">What's the verdict?</h3>
<p>So I've been using Pulumi for a few weeks now, and here are my impressions.</p>
<p>It's <strong>really fast compared to Terraform</strong>. I didn't even expect this one, but for some reason Pulumi manages to at least "feel" a lot faster. I haven't done any measurements to verify this, but it definitely feels much faster.</p>
<p>Their <strong>documentation is really good</strong>, although it took me a bit to learn to navigate it.</p>
<p>Having <strong>type annotations</strong> is great! This makes it a lot more friendly to work with as a developer. No more passing around just plain strings in your code. However the type annotations doesn't quite work in PyCharm because of how the Pulumi code is written. I consider this a PyCharm bug though, and I expect it to be fixed in the future. For now I can simply <em>ctrl+b</em> the class in question, and read the type annotations from the source code.</p>
<p>The <strong>Pulumi (Web) Console is a neat way to look at your infrastructure</strong>. I know there are similar solutions for Terraform, but I hadn't used them personally.</p>
<p>The <strong>developer ergonomics are great. Using your language of choice helps a lot.</strong> When running a company of one, like myself, it's really important to limit the number of things you need to learn as much as possible. While HCL is simple it's yet another language to remember. If you're working at a larger company it's a great way to encourage developers to do more infrastructure work.</p>
<p>Pulumi is an <strong>open core type product</strong>, and you will have to pay to access the features on the higher tiers. Their current plans are very fairly priced. I myself is using the free community plan, which works great for me. There's obviously the risk that they will remove the free plan in the future. However even if that happens I could either pay the fee, or transfer to state storage to AWS S3, and retain most of the product functionality. This feels like an acceptable level of risk for me.</p>
<p>My goal was to do the whole migration in one day. In reality it ended up being closer to two days. However, given that I learned Pulumi and migrated ~130 resources in that amount of time I'd say that Pulumi is quite easy to get started with.</p>
<hr />
<p><strong>All in all, I'm very happy with making the transition to Pulumi! I became way more productive managing my infrastructure. At the same time I ended up enjoying the work  of doing so a lot more - which is really important too.</strong></p>
<p><em>P.S.
I'm still a Pulumi n00b, so there's probably better ways to do certain things than what I described here :).</em></p>
]]></content:encoded></item><item><title><![CDATA[The 3 most important questions to ask yourself when creating a product you plan to sell]]></title><description><![CDATA[As I mentioned in my last post I took a break for a few weeks after leaving my last job, to recharge my batteries. I'm terrible at disconnecting from work, and this time was no exception (this is something I need to work on).
After a couple of days, ...]]></description><link>https://blog.ekik.org/the-3-most-important-questions-to-ask-yourself-when-creating-a-product-you-plan-to-sell</link><guid isPermaLink="true">https://blog.ekik.org/the-3-most-important-questions-to-ask-yourself-when-creating-a-product-you-plan-to-sell</guid><dc:creator><![CDATA[Erik Näslund]]></dc:creator><pubDate>Sat, 10 Apr 2021 16:16:30 GMT</pubDate><content:encoded><![CDATA[<p>As I mentioned in my last post I took a break for a few weeks after leaving my last job, to recharge my batteries. I'm terrible at disconnecting from work, and this time was no exception (this is something I need to work on).</p>
<p>After a couple of days, I was itching to do something work-like, so I started playing with  <a target="_blank" href="https://fastapi.tiangolo.com">FastAPI</a> to see if it could be what I'd use for my next project (I'll tell you all about it in a few weeks).</p>
<p>I even got so excited that I started creating visual prototypes using <a target="_blank" href="https://www.figma.com">Figma</a> of what the product would look like. Once I had the prototypes I created backlogs and started brainstorming more ideas with myself. In a matter of days, I had some proof of concept code and a plan, and I felt really pumped to build this!</p>
<h3 id="is-this-such-a-bad-way-to-build-a-product-i-had-a-good-flow-going-right">Is this such a bad way to build a product? I had a good flow going, right?</h3>
<p>I honestly think I would have been off sipping Mojito's on the beach for the last couple of weeks. At least I managed to stop myself in time - let me explain...</p>
<p>Since I'm a developer at heart I always tend to focus on the product first. I get overly excited as soon as I think of something cool I could build. My mind starts spiralling down the path of implementation details, and I start building a high-level mental model of how it would all work.</p>
<p>Luckily enough I stopped here and allowed myself some time to think. Eventually, I came up with the three questions below. <em>These are the three most important things you should ask yourself before starting to develop a new product</em>.</p>
<h3 id="question-1-can-i-build-this">Question 1 - "Can I build this?"</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1618003875667/2j5XC11gV.png" alt="undraw_building_blocks_n0nc.png" /></p>
<p>How do I build this idea I have in my head? It quickly tends to go into implementation concerns like "what will the product look like", "will it support feature X" and "which language and framework should I use"?</p>
<p>We developers are spoiled with a plethora of excellent technology to help us do rapid application development. It's more likely than not that you can put together the software you need, assuming you are a developer yourself. There might be some struggles along the way, but as long as you know (with reasonable certainty) that you can find a way to build it, then you can tick this box.</p>
<p>Small companies like my own might have to scrap an idea because we can't build it given the people we have available. That's perfectly fine - the idea might not just be for us to execute on.</p>
<p><em>TIP: Try to go for a "trivial" project to start out with. Things always take longer than we think they do anyway :)</em>.</p>
<h3 id="question-2-can-i-get-others-to-use-this">Question 2 - "Can I get others to use this?"</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1618003919714/uFytstqVR.png" alt="undraw_deliveries_131a.png" /></p>
<p>This is where things get interesting, and where I used to have a big blind spot.</p>
<p>Once you've built a product, how do you get other people to use it?</p>
<p>My first ever project operated on the principle of "if you build it they will come" (spoiler alert - that's not how it works). The second time around I had the help of a lot of great marketers, who managed to get the distribution we needed.</p>
<p>But how about this time around when I don't have a group of marketers to help out?</p>
<p><em>TIP: Expose your product in every way possible as soon as you can. Often earlier than you're comfortable with. If you can't find people to give you feedback, or test your prototypes, then you most likely won't find people to buy the finished product either.</em></p>
<p><em>Don't fall into the trap of thinking that you need a finished product. As soon as you have an idea or a screenshot that is something you can show off!</em></p>
<p><em>The <a target="_blank" href="https://dominikdotzauer.de/sales-safari/">Sales Safari</a> by Amy Hoy is a great read exploring how to find the "watering holes" where your potential customers hang out.</em></p>
<h3 id="question-3-can-i-get-paid-for-this">Question 3 - "Can I get paid for this?"</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1618003977196/GYVQsViSU.png" alt="undraw_online_payments_luau.png" /></p>
<p>There's a clear difference between being willing to use something, and willing to pay for it. I for example use a web browser like everyone else, but it's very unlikely I'd be willing to pay for one (unless it was drastically better for some reason).</p>
<p>Depending on your earning goals you might give high or low importance to this question. I believe it's generally less important if you have a company of one, like myself. The reason is simple - you don't have a lot of salaries to pay.
People are often willing to pay <em>something</em> (even if it's a small sum) for most products.</p>
<p>If you have an idea in mind where you don't think there's any <a target="_blank" href="https://www.priceintelligently.com/blog/willingness-to-pay">willingness to pay</a> at all you might want to avoid turning it into a business.</p>
<p><em>TIP: Think about your financial goals with the product. What would happen if you didn't reach them? Are you OK with just running it as a side business?</em></p>
<h3 id="putting-it-all-together">Putting it all together</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1618006600171/waVftm6DQ.png" alt="venn.png" /></p>
<p>So now that you've asked yourself these three questions you can place your ideas in the Venn diagram above. Do enough research for each question to be able to answer it somewhat confidently. You can never get 100% certain of course, but just asking yourself these questions is a great start!</p>
<table>
<thead>
<tr>
<td>Can build</td><td>Can get others to use</td><td>Can get paid for</td><td>Product type example</td></tr>
</thead>
<tbody>
<tr>
<td>✅</td><td>❌</td><td>❌</td><td>Personal product</td></tr>
<tr>
<td>✅</td><td>✅</td><td>❌</td><td>Free open source product</td></tr>
<tr>
<td>✅</td><td>✅</td><td>✅</td><td>Viable business product</td></tr>
<tr>
<td>❌</td><td>✅</td><td>✅</td><td>Outsourced development product</td></tr>
<tr>
<td>...</td><td>...</td><td>...</td><td>...</td></tr>
</tbody>
</table>
<p>Depending on where your product idea fits within the diagram you can see what type of product it might be. The table above is just an example of some different product types.</p>
<p>For example - If you can build your idea, and you can get others to use it, but don't see a way to get paid - then it might be a great idea for a free open source product. Leverage the community to make it as awesome as it possibly can be!</p>
<p>This is the stage where you might realise that while your idea might be awesome, it might not be the right idea to build a business around.</p>
<h3 id="what-am-i-gonna-do-next">What am I gonna do next?</h3>
<p>There's this  <a target="_blank" href="https://twitter.com/justinkan/status/1059989657218248704?lang=en">quote</a>  you might have heard:</p>
<blockquote>
<p>First time founders are obsessed with product.</p>
<p>Second time founders are obsessed with distribution.</p>
</blockquote>
<p>I can definitely relate to being too obsessed with the product, so this time I'm going to address the distribution ("can I get people to use it?") a bit more before I go full steam ahead on building something.</p>
<p>Next up is scheduling some time to talk with the people I <em>think</em> would be the users of my product, to talk a bit about the issues they're facing in the area.</p>
<p>I won't lie - it's <strong>really</strong> scary going out of my comfort zone scheduling meetings with people I don't know! I guess that's why I put it off for so long. I recently read <a target="_blank" href="http://momtestbook.com">The Mom Test</a> and it helped to lessen my fears a tiny bit.</p>
<p>Wish me luck! I'll post an update here on the blog once the meetings are done, with some details about what I learned. Hopefully, that'll make it a bit easier for you when it's your time to schedule those terrifying meetings with potential future users :)</p>
]]></content:encoded></item><item><title><![CDATA[I co-founded one of the biggest behavioural analytics companies in the world]]></title><description><![CDATA[Let's rewind a bit and tell you a bit about  Hotjar, myself, and how I ended up where I am today.
Moving to Malta
I was born and raised in Sweden but moved to Malta in 2009, so I guess I should call myself Swed-tese by now. I had friends who did the ...]]></description><link>https://blog.ekik.org/i-co-founded-one-of-the-biggest-behavioural-analytics-companies-in-the-world</link><guid isPermaLink="true">https://blog.ekik.org/i-co-founded-one-of-the-biggest-behavioural-analytics-companies-in-the-world</guid><dc:creator><![CDATA[Erik Näslund]]></dc:creator><pubDate>Wed, 24 Mar 2021 15:34:39 GMT</pubDate><content:encoded><![CDATA[<p>Let's rewind a bit and tell you a bit about  <a target="_blank" href="https://www.hotjar.com">Hotjar</a>, myself, and how I ended up where I am today.</p>
<p><strong>Moving to Malta</strong></p>
<p>I was born and raised in Sweden but moved to Malta in 2009, so I guess I should call myself Swed-tese by now. I had friends who did the move a few years ahead of me. After visiting just once I knew it was the place where I wanted to be. It had this great atmosphere, and a much more sensible and relaxed approach to life, at least in my opinion.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1616521847008/ec3nTYUkN.png" alt="image.png" /></p>
<p>While living in Sweden I felt trapped in the hamster wheel of life, running towards something I didn't necessarily even know why I was trying to get to. Was I making a significant enough difference? Did what I do really matter?</p>
<p>So eventually I packed my bags and moved to Malta. Being an English-speaking EU country made the move really easy, and in a few weeks I had an apartment contract signed, and ready to start working at this place that was called Uniblue.</p>
<p><strong>Co-founding Hotjar</strong></p>
<p>During the early part of 2014, I got the amazing opportunity to be a co-founder of Hotjar. The other four co-founders were people I got to know from Uniblue. I still don't feel I deserved the chance, but apparently I did something to impress them enough to offer me to be a co-founder.</p>
<p>We built the core product in about six months, and we were off to the races. What happened then is something that still amazes me to this day - we got this incredible traction and today more than 900 000(!) organizations are using Hotjar.</p>
<p>We eventually realized we needed to start hiring a lot of people, and today Hotjar have more than 150 employees.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1616523963925/sLWBP8j4J.png" alt="image.png" /></p>
<p><strong>That sounds like an amazing growth story!</strong></p>
<p>The first few years were super intense, and I think we all learned more than we ever had before about how to run a business.</p>
<p>However, recently I found myself thinking - am I back in the hamster wheel that I escaped from in the first place? After many sessions of discussing this with myself I realized that <em>"yeah, I'm kinda back in the hamster wheel"</em>.</p>
<p>I had helped build the coolest company I ever worked at - but personally, I wasn't really happy anymore. I recently realized that I haven't been reflecting enough on important things in my life, and instead I kept myself busy with work. Once the gears started turning it became more and more obvious what I needed to do - leave the company I co-founded.</p>
<blockquote>
<p>I don't really enjoy being a cog in a well-oiled machine. I thrive in the more chaotic environment you get in a tiny company, where you always have to wear multiple hats.</p>
<p>// Erik, in one of his late-night conversations with himself</p>
</blockquote>
<p>So let's bring out an old classic - <em>It's not you, it's me</em>. In the case of myself and Hotjar it's really how it is though. I had helped to grow an awesome company that I'm so incredibly proud of - but a company that wasn't a place where I did my best work anymore. </p>
<p><strong>So what's next?</strong></p>
<p>I'm going back to doing what I love - <em>creating and running a tiny company from scratch</em>, but all by myself this time.</p>
<p> <a target="_blank" href="https://www.amazon.com/Company-One-Staying-Small-Business/dp/1328972356"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1616524858008/p7E8dzs1s.png" alt="image.png" /></a> </p>
<p>This book - "Company of One" actually helped me a lot. Much of what I read resonated so well with me, and I was finally able to realize what I wanted out of my work:</p>
<ul>
<li>Spend my time doing things that I think are a net positive for humanity.</li>
<li>Build my work around my life and not the other way around.</li>
<li>Say no to opportunities that would require the company to grow in a way that wouldn't fit with the life I want to live.</li>
</ul>
<p>I'm certain I wouldn't be able to pull this off if I tried to before my years with Hotjar. I was simply too naive and lacked a lot of the skills I've gathered over the years. I'll be forever thankful for what I learned at Hotjar, and all the special moments we shared!</p>
<p><strong>If you're up for it, subscribe to my blog and follow my adventure of founding a new company from scratch. If you're interested in doing the same one day you might get a good tip or two. If you just wanna see if I make it that's fine too :)</strong></p>
<p>I plan to write a new post about once a week. However, I'm going to start off by taking a couple of weeks of time off to recharge. The last seven years have been amazing, but demanding - I think I deserve a short break :p</p>
]]></content:encoded></item></channel></rss>