<?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[FOSS Experience]]></title><description><![CDATA[FOSS Experience]]></description><link>https://fossexperience.wawrzynczuk.com</link><generator>RSS for Node</generator><lastBuildDate>Wed, 15 Apr 2026 03:53:29 GMT</lastBuildDate><atom:link href="https://fossexperience.wawrzynczuk.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Using tags to express Ansible tasks dependencies]]></title><description><![CDATA[Everything is simple when we write a playbook that does one thing. We have a powerful tool for expressing dependencies: the task order. But sometimes we want more than one target. We want to deploy something, then cleanup something. Or even build som...]]></description><link>https://fossexperience.wawrzynczuk.com/using-tags-to-express-ansible-tasks-dependencies</link><guid isPermaLink="true">https://fossexperience.wawrzynczuk.com/using-tags-to-express-ansible-tasks-dependencies</guid><category><![CDATA[ansible]]></category><category><![CDATA[automation]]></category><category><![CDATA[ci-cd]]></category><dc:creator><![CDATA[Maciej Wawrzyńczuk]]></dc:creator><pubDate>Tue, 03 Feb 2026 09:27:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1770108099926/d45899c7-97f7-4f56-a933-10befc51a48c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Everything is simple when we write a playbook that does one thing. We have a powerful tool for expressing dependencies: the task order. But sometimes we want more than one target. We want to deploy something, then cleanup something. Or even build something else. For example, we may want a standard CI/CD workflow:</p>
<ul>
<li><p>Build a Docker image from source.</p>
</li>
<li><p>Push the image to a registry.</p>
</li>
<li><p>Update a K8s deployment with the new image.</p>
</li>
</ul>
<p>The playbook structure would be:</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">image...</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Push</span> <span class="hljs-string">to</span> <span class="hljs-string">a</span> <span class="hljs-string">registry...</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Update</span> <span class="hljs-string">deployment...</span>
</code></pre>
<p>Now, your boss comes and says "don't publish anything before I tell you so". We add tags to run tasks separately:</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">image...</span>
  <span class="hljs-attr">tags:</span> [<span class="hljs-string">build</span>]
<span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Push</span> <span class="hljs-string">to</span> <span class="hljs-string">registry...</span>
  <span class="hljs-attr">tags:</span> [<span class="hljs-string">publish</span>]
<span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Update</span> <span class="hljs-string">deployment...</span>
  <span class="hljs-attr">tags:</span> [<span class="hljs-string">deploy</span>]
</code></pre>
<p>Almost there. We can do everything we want, but we need to specify multiple tags. You can create a serious mess this way. So, the third and final approach is to populate a tag to every tag we're dependent on:</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">image...</span>
  <span class="hljs-attr">tags:</span> [<span class="hljs-string">build</span>, <span class="hljs-string">publish</span>, <span class="hljs-string">deploy</span>]
<span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Push</span> <span class="hljs-string">to</span> <span class="hljs-string">registry...</span>
  <span class="hljs-attr">tags:</span> [<span class="hljs-string">publish</span>, <span class="hljs-string">deploy</span>]
<span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Update</span> <span class="hljs-string">deployment...</span>
  <span class="hljs-attr">tags:</span> [<span class="hljs-string">deploy</span>]
</code></pre>
<p>Here you go. If you want to deploy all - use tag 'deploy'. For build only - 'build'. That would be it, except for one caveat. We don't want to run unnecessary tasks. Theoretically - it shouldn't be a problem. Ansible modules are meant to be idempotent. It means - we can run a task any number of times we want - the result should be the same. But sometimes we're wasting our time. Then it would be good to have a 'when' clause to check whether running a task is really necessary.</p>
<p>And one bonus thing. If you create a complex playbook with multiple targets, having a default workflow, without tags makes no sense. For example, you don't want to build something then destroy it. In that case - use a default tag guard:</p>
<pre><code class="lang-yaml"> <span class="hljs-attr">pre_tasks:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Default</span> <span class="hljs-string">tag</span> <span class="hljs-string">guard</span>
      <span class="hljs-attr">assert:</span>
        <span class="hljs-attr">that:</span> <span class="hljs-string">"'all' not in ansible_run_tags"</span>
        <span class="hljs-attr">msg:</span> <span class="hljs-literal">No</span> <span class="hljs-string">default</span> <span class="hljs-string">tasks</span> <span class="hljs-string">here.</span>
</code></pre>
<p>Thanks for the audience. I hope I managed to write something which is not obvious for everyone.</p>
]]></content:encoded></item><item><title><![CDATA[How to automate Terraform deployments with GitHub Actions.]]></title><description><![CDATA[I know… There is lots of articles on this rather basic topic. I’m not consider myself an expert. Maybe someone find helpful a HOWTO on this particular combination of technologies.
TL;DR
I’m going to create a simple workflow for deploying a Terraform ...]]></description><link>https://fossexperience.wawrzynczuk.com/how-to-automate-terraform-deployments-with-github-actions</link><guid isPermaLink="true">https://fossexperience.wawrzynczuk.com/how-to-automate-terraform-deployments-with-github-actions</guid><category><![CDATA[AWS]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[github-actions]]></category><category><![CDATA[automation]]></category><category><![CDATA[ci-cd]]></category><dc:creator><![CDATA[Maciej Wawrzyńczuk]]></dc:creator><pubDate>Wed, 14 Jan 2026 17:28:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768298799487/d5cb514b-bebc-4783-9a44-f296f33c1625.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I know… There is lots of articles on this rather basic topic. I’m not consider myself an expert. Maybe someone find helpful a HOWTO on this particular combination of technologies.</p>
<h1 id="heading-tldr">TL;DR</h1>
<p>I’m going to create a simple workflow for deploying a Terraform stack on AWS using GitHub Action. I will setup OIDC authentication between GitHub and AWS.</p>
<h1 id="heading-prerequisites">Prerequisites</h1>
<p>We will need:</p>
<ul>
<li><p>a simple, working Terraform project,</p>
</li>
<li><p>an AWS account.</p>
</li>
<li><p>a GitHub account.</p>
</li>
</ul>
<h1 id="heading-aws-identity-provider">AWS identity provider</h1>
<p>Please, create it according to the guide: <a target="_blank" href="https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-aws">https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-aws</a></p>
<p>It needs to be done once per AWS account.</p>
<h1 id="heading-s3-backend">S3 backend</h1>
<p>It’s not going to work with a local state file. Let’s prepare all required items. I’m going to use Terraform code. Please mind it is not a part of the project you’re going to deploy.</p>
<h2 id="heading-bucket">Bucket</h2>
<p>Just create a bucket with permissions.</p>
<pre><code class="lang-plaintext">resource "aws_s3_bucket" "state-bucket" {
  bucket = local.bucket_name
}

resource "aws_s3_bucket_versioning" "vers" {
  bucket = aws_s3_bucket.state-bucket.id
  versioning_configuration {
    status = "Enabled"
  }
}
</code></pre>
<p>I have a local variable describing the bucket name.</p>
<h2 id="heading-iam">IAM</h2>
<p>I’m going to use the same permissions for backend and for AWS provider. You probably should separate these roles for non-lab environments.</p>
<h3 id="heading-local-user">Local user</h3>
<p>Despite - we’re going to use OIDC - I still want and user to run it locally.</p>
<pre><code class="lang-plaintext">data "aws_iam_policy_document" "project_policy" {
  statement {
    actions = [
      "s3:ListBucket"
    ]
    resources = [aws_s3_bucket.state-bucket.arn]
  }

  statement {
    actions = [
      "s3:GetObject",
      "s3:PutObject",
      "s3:DeleteObject"
    ]
    resources = [
      "${aws_s3_bucket.state-bucket.arn}/*/terraform.tfstate",
      "${aws_s3_bucket.state-bucket.arn}/*/terraform.tfstate.tflock"
    ]
  }
}

resource "aws_iam_user" "project_user" {
  name = "${var.project_name}-user"
}

resource "aws_iam_user_policy" "project_user_policy" {
  user   = aws_iam_user.project_user.id
  policy = data.aws_iam_policy_document.project_policy.json
}
</code></pre>
<p>Now - add the permissions which allow to build you infra. They are a little bit too wide. Please adjust them to your needs.</p>
<pre><code class="lang-plaintext">resource "aws_iam_user_policy_attachment" "ec2-full" {
  user       = aws_iam_user.project_user.id
  policy_arn = "arn:aws:iam::aws:policy/AmazonEC2FullAccess"
}

resource "aws_iam_user_policy_attachment" "vpc-full" {
  user       = aws_iam_user.project_user.id
  policy_arn = "arn:aws:iam::aws:policy/AmazonVPCFullAccess"
}

resource "aws_iam_user_policy_attachment" "ssm-ro" {
  user       = aws_iam_user.project_user.id
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess"
}

resource "aws_iam_user_policy_attachment" "iam-ro" {
  user       = aws_iam_user.project_user.id
  policy_arn = "arn:aws:iam::aws:policy/IAMReadOnlyAccess"
}
</code></pre>
<p>You will need an access key for the user to use it locally</p>
<h3 id="heading-role">Role</h3>
<p>OIDC needs an IAM role.</p>
<pre><code class="lang-plaintext">data "aws_iam_policy_document" "sts_policy" {
  statement {
    effect = "Allow"
    principals {
      type        = "Federated"
      identifiers = ["arn:aws:iam::&lt;AWS account ID&gt;:oidc-provider/token.actions.githubusercontent.com"]
    }
    actions = ["sts:AssumeRoleWithWebIdentity"]
    condition {
      test     = "StringLike"
      variable = "token.actions.githubusercontent.com:sub"
      values = [
        "repo:&lt;GitHub account&gt;/&lt;Repo&gt;:ref:refs/heads/main"
      ]
    }
  }
}


resource "aws_iam_role" "project_role" {
  assume_role_policy = data.aws_iam_policy_document.sts_policy.json
  name               = "${var.project_name}-role"
}

resource "aws_iam_role_policy" "project_role_attach" {
  role   = aws_iam_role.project_role.id
  policy = data.aws_iam_policy_document.project_policy.json
}
</code></pre>
<p>You need to adjust the following parameters:</p>
<ul>
<li><p>AWS account ID,</p>
</li>
<li><p>GitHub user or organization,</p>
</li>
<li><p>The repo name.</p>
</li>
</ul>
<p>The last resource allows the role to access the bucket. You still need the resources access. You can copy the code from user changing <code>aws_iam_user_policy_attachment</code> to <code>aws_iam_role_policy_attachment</code>.</p>
<p>That’s it. You can deploy the resources manually.</p>
<h1 id="heading-setup-the-s3-backend-for-you-project">Setup the S3 backend for you project</h1>
<p>Now create or update the backend setup for your project.</p>
<pre><code class="lang-plaintext">terraform {
  backend "s3" {
    bucket       = "&lt;bucket-name&gt;"
    key          = "&lt;stack&gt;/terraform.tfstate"
    use_lockfile = true
    region       = "eu-central-1"
  }
}
</code></pre>
<p>Replace bucket name, and “folder” in the bucket. I have multiple stacks in my project - each with a separate state. If your project is live already, use <code>terraform init -migrate-state</code>. If i’s a fresh one - just <code>terraform init'</code></p>
<h1 id="heading-workflow-setup">Workflow setup</h1>
<p>Crate a YAML file in <code>.github/workflows/</code></p>
<pre><code class="lang-yaml"><span class="hljs-meta">---</span>
<span class="hljs-attr">name:</span> <span class="hljs-string">Net</span>
<span class="hljs-attr">on:</span>
  <span class="hljs-attr">push:</span>
    <span class="hljs-attr">branches:</span> [<span class="hljs-string">"main"</span>]
    <span class="hljs-attr">paths:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">net/**</span>

<span class="hljs-attr">permissions:</span>
  <span class="hljs-attr">id-token:</span> <span class="hljs-string">write</span>
  <span class="hljs-attr">contents:</span> <span class="hljs-string">read</span>

<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">make_key:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">defaults:</span>
      <span class="hljs-attr">run:</span>
        <span class="hljs-attr">working-directory:</span> <span class="hljs-string">net/</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v6</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">aws-actions/configure-aws-credentials@v5</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">role-to-assume:</span> <span class="hljs-string">arn:aws:iam::&lt;AWS</span> <span class="hljs-string">account</span> <span class="hljs-string">id&gt;:role/tf-infra-role</span>
          <span class="hljs-attr">aws-region:</span> <span class="hljs-string">eu-central-1</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">hashicorp/setup-terraform@v3</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span> <span class="hljs-string">terraform</span> <span class="hljs-string">init</span> <span class="hljs-string">-input=false</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span> <span class="hljs-string">terraform</span> <span class="hljs-string">plan</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span> <span class="hljs-string">terraform</span> <span class="hljs-string">apply</span> <span class="hljs-string">-auto-approve</span>
</code></pre>
<p>A quick explanation:</p>
<ul>
<li><p>the workflow runs when you push to the ‘main’ and something in net/ subdir is changed. As I said - I have multiple stacks with multiple workflows.</p>
</li>
<li><p>“permissions” - required to handle OIDC.</p>
</li>
<li><p>“working-directory” - skip it if you run your project in the root.</p>
</li>
<li><p>“uses: aws-actions/configure-aws-credentials” - setups AWS connectivity. Use your actual account id.</p>
</li>
<li><p>Then terraform stuff goes. I know. I should have an approval after plan…</p>
</li>
</ul>
<p>If everything went right - after next push you infra should built. You always can run <code>terraform destroy</code> locally.</p>
<h1 id="heading-references">References</h1>
<p><a target="_blank" href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp.html">https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp.html</a></p>
<p><a target="_blank" href="https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-aws">https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-aws</a></p>
]]></content:encoded></item><item><title><![CDATA[How to create a minimal VPC in AWS with Terraform.]]></title><description><![CDATA[Don’t want use the default one anymore? Deleted the default one? Good. Let’s do it. Actually it is minimal excepting one detail, my fetish - IPv6 support.
VPC
Of course, to create a VDP - we need VDP:
resource "aws_vpc" "min_vpc" {
  cidr_block      ...]]></description><link>https://fossexperience.wawrzynczuk.com/how-to-create-a-minimal-vpc-in-aws-with-terraform</link><guid isPermaLink="true">https://fossexperience.wawrzynczuk.com/how-to-create-a-minimal-vpc-in-aws-with-terraform</guid><category><![CDATA[Terraform]]></category><category><![CDATA[#IaC]]></category><category><![CDATA[AWS]]></category><dc:creator><![CDATA[Maciej Wawrzyńczuk]]></dc:creator><pubDate>Sun, 04 Jan 2026 20:12:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1767547740879/876784c5-1b73-4f4a-8c75-16653c9c51b6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Don’t want use the default one anymore? Deleted the default one? Good. Let’s do it. Actually it is minimal excepting one detail, my fetish - IPv6 support.</p>
<h1 id="heading-vpc">VPC</h1>
<p>Of course, to create a VDP - we need VDP:</p>
<pre><code class="lang-plaintext">resource "aws_vpc" "min_vpc" {
  cidr_block                       = "10.0.0.0/16"
  assign_generated_ipv6_cidr_block = true
  enable_dns_support               = true
  enable_dns_hostnames             = true
}
</code></pre>
<p>We need to specify:</p>
<ul>
<li><p>IP range. (Do you have any idea why people hate 172.16/12?)</p>
</li>
<li><p>IPv6 range. It’s assigned automatically.</p>
</li>
<li><p>DNS support. If the instances uses Amazon DNS. Actually true is default.</p>
</li>
<li><p>DNS hostnames. If true - instances get funny hostnames in amazonaws.com.</p>
</li>
</ul>
<h1 id="heading-subnet">Subnet</h1>
<p>Every VPC needs at least one subnet.</p>
<pre><code class="lang-plaintext">resource "aws_subnet" "pub" {
  vpc_id                          = aws_vpc.min_vpc.id
  cidr_block                      = cidrsubnet(aws_vpc.min_vpc.cidr_block, 8, 0)
  ipv6_cidr_block                 = cidrsubnet(aws_vpc.min_vpc.ipv6_cidr_block, 8, 0)
  assign_ipv6_address_on_creation = true
}
</code></pre>
<ul>
<li><p>vpc_id - the parent VPC.</p>
</li>
<li><p>cidr_block - the address range of the segment. Similar for IPv6. We don’t specify it manually - instead we’re using cidrsubnet function. Which cuts a network segments to a smaller one.</p>
</li>
</ul>
<h1 id="heading-gateway">Gateway</h1>
<p>Like every gateway - traffic goes through it.</p>
<pre><code class="lang-plaintext">resource "aws_internet_gateway" "gw" {
  vpc_id = aws_vpc.min_vpc.id
}
</code></pre>
<p>Nothing fancy. Just VPC id.</p>
<h1 id="heading-routing-table">Routing table</h1>
<p>Just routes everything to the gateway.</p>
<pre><code class="lang-plaintext">resource "aws_route_table" "rt" {
  vpc_id = aws_vpc.min_vpc.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.gw.id
  }
  route {
    ipv6_cidr_block = "::0/0"
    gateway_id      = aws_internet_gateway.gw.id
  }
}
</code></pre>
<h1 id="heading-route-table-association">Route table association</h1>
<p>Connects a route to a subnet.</p>
<pre><code class="lang-plaintext">resource "aws_route_table_association" "pub_assoc" {
  subnet_id      = aws_subnet.pub.id
  route_table_id = aws_route_table.rt.id
}
</code></pre>
<h1 id="heading-final-words">Final words</h1>
<p>That’s more or less it. You can remove IPv6 related lines. You’ll probably gonna need a security group. I’ll describe it along with an instance example - soon.</p>
<h1 id="heading-references">References</h1>
<p><a target="_blank" href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc">https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc</a></p>
<p><a target="_blank" href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/subnet">https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/subnet</a></p>
<p><a target="_blank" href="https://developer.hashicorp.com/terraform/language/functions/cidrsubnet">https://developer.hashicorp.com/terraform/language/functions/cidrsubnet</a></p>
<p><a target="_blank" href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/internet_gateway.html">https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/internet_gateway.html</a></p>
<p><a target="_blank" href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table">https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table</a></p>
]]></content:encoded></item><item><title><![CDATA[Setup S3 backend for Terraform]]></title><description><![CDATA[It may happen that you starting working on a Terraform project in a team and local state file doesn’t work for you anymore. Time to a remote backed. It’s not going to be complete documentation. Or better than others. But maybe I’ll manage to hit your...]]></description><link>https://fossexperience.wawrzynczuk.com/setup-s3-backend-for-terraform</link><guid isPermaLink="true">https://fossexperience.wawrzynczuk.com/setup-s3-backend-for-terraform</guid><category><![CDATA[Terraform]]></category><category><![CDATA[#IaC]]></category><category><![CDATA[AWS]]></category><category><![CDATA[S3]]></category><dc:creator><![CDATA[Maciej Wawrzyńczuk]]></dc:creator><pubDate>Mon, 29 Dec 2025 17:08:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1767021086713/35e5e8e7-f7dc-49ee-97b2-1aaef0f0301e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It may happen that you starting working on a Terraform project in a team and local state file doesn’t work for you anymore. Time to a remote backed. It’s not going to be complete documentation. Or better than others. But maybe I’ll manage to hit your needs. If you want more comprehensive documentation - please take a look at “Links” section.</p>
<p>We’re going to use:</p>
<ul>
<li><p>S3 bucket with versioning and encryption.</p>
</li>
<li><p>DynamoDB table for locking.</p>
</li>
<li><p>Separate user for backend access.</p>
</li>
<li><p>Separate user backend user for each project.</p>
</li>
</ul>
<p>To follow the the guide you gonna need:</p>
<ul>
<li><p>AWS account. Root or enough privileges to manage users and create resources.</p>
</li>
<li><p>aws cli installed.</p>
</li>
<li><p>Terraform installed.</p>
</li>
</ul>
<p>You also need to know:</p>
<ul>
<li><p>The project name.</p>
</li>
<li><p>The bucket name. They need to be unique globally. It would be useful to find your favorite name and add a random suffix. Like string generated with the command <code>openssl rand -hex 3</code>. Or - you can read guide mentioned in links.</p>
</li>
</ul>
<h1 id="heading-creating-aws-resources">Creating AWS resources</h1>
<p>First - login to you AWS admin account. You can create your bucket now. Adjust your bucket name and region.</p>
<pre><code class="lang-bash">aws s3api create-bucket \
  --bucket tf-state-xxxx \
  --region eu-central-1 \
  --create-bucket-configuration LocationConstraint=eu-central-1
</code></pre>
<p>Enable bucket versioning.</p>
<pre><code class="lang-bash">aws s3api put-bucket-versioning \
  --bucket tf-state-xxxx \
  --versioning-configuration Status=Enabled
</code></pre>
<p>Then encryption.</p>
<pre><code class="lang-bash">aws s3api put-bucket-encryption \
  --bucket  tf-state-xxxx\
  --server-side-encryption-configuration <span class="hljs-string">'{
    "Rules": [{
      "ApplyServerSideEncryptionByDefault": {
        "SSEAlgorithm": "AES256"
      }
    }]
  }'</span>
</code></pre>
<p>Create DynamoDB table:</p>
<pre><code class="lang-bash">aws dynamodb create-table \
  --table-name terraform-locks \
  --attribute-definitions AttributeName=LockID,AttributeType=S \
  --key-schema AttributeName=LockID,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST
</code></pre>
<h1 id="heading-create-user">Create user</h1>
<p>Please create a file from following template, replacing bucket name, project name, client id and region. The name used in the example is. <code>projectA-policy.json</code>. Feel free to adjust.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"Version"</span>: <span class="hljs-string">"2012-10-17"</span>,
  <span class="hljs-attr">"Statement"</span>: [
    {
      <span class="hljs-attr">"Sid"</span>: <span class="hljs-string">"AllowS3StateAccess"</span>,
      <span class="hljs-attr">"Effect"</span>: <span class="hljs-string">"Allow"</span>,
      <span class="hljs-attr">"Action"</span>: [
        <span class="hljs-string">"s3:GetObject"</span>,
        <span class="hljs-string">"s3:PutObject"</span>,
        <span class="hljs-string">"s3:DeleteObject"</span>
      ],
      <span class="hljs-attr">"Resource"</span>: <span class="hljs-string">"arn:aws:s3:::&lt;bucket-name&gt;/&lt;project-name&gt;/*"</span>
    },
    {
      <span class="hljs-attr">"Sid"</span>: <span class="hljs-string">"AllowS3ListPrefix"</span>,
      <span class="hljs-attr">"Effect"</span>: <span class="hljs-string">"Allow"</span>,
      <span class="hljs-attr">"Action"</span>: <span class="hljs-string">"s3:ListBucket"</span>,
      <span class="hljs-attr">"Resource"</span>: <span class="hljs-string">"arn:aws:s3:::&lt;bucket-name&gt;"</span>,
      <span class="hljs-attr">"Condition"</span>: {
        <span class="hljs-attr">"StringLike"</span>: {
          <span class="hljs-attr">"s3:prefix"</span>: <span class="hljs-string">"&lt;project-name&gt;/*"</span>
        }
      }
    },
    {
      <span class="hljs-attr">"Sid"</span>: <span class="hljs-string">"AllowDynamoDBLocking"</span>,
      <span class="hljs-attr">"Effect"</span>: <span class="hljs-string">"Allow"</span>,
      <span class="hljs-attr">"Action"</span>: [
        <span class="hljs-string">"dynamodb:GetItem"</span>,
        <span class="hljs-string">"dynamodb:PutItem"</span>,
        <span class="hljs-string">"dynamodb:DeleteItem"</span>,
        <span class="hljs-string">"dynamodb:UpdateItem"</span>
      ],
      <span class="hljs-attr">"Resource"</span>: <span class="hljs-string">"arn:aws:dynamodb:eu-central-1:&lt;client-id&gt;:table/terraform-locks"</span>
    }
  ]
}
</code></pre>
<p>create the user:</p>
<pre><code class="lang-bash">aws iam create-user --user-name projectA-tfstate
</code></pre>
<p>Attach the policy.</p>
<pre><code class="lang-bash">aws iam put-user-policy \
  --user-name projectA-tfstate \
  --policy-name projectA-tfstate-policy \
  --policy-document file://projectA-policy.json
</code></pre>
<p>Create access key:</p>
<pre><code class="lang-bash">aws iam create-access-key --user-name projectA-terraform
</code></pre>
<p>Create an aws cli profile.</p>
<pre><code class="lang-bash">aws sonfigure --profile projectA-terraform
</code></pre>
<h1 id="heading-start-using-it">Start using it</h1>
<p>Create a Terraform file. For example backend.tf.</p>
<pre><code class="lang-plaintext">terraform {
  backend "s3" {
    bucket         = "tf-state-xxxx"
    key            = "projectA-terraform/terraform.tfstate"
    region         = "eu-central-1"
    profile        = "projectA-terraform"
    use_lockfile   = true
    encrypt        = true
  }
}
</code></pre>
<p>If you’re lucky - you can run <code>terraform init</code> without errors now.</p>
<h1 id="heading-links">Links</h1>
<p><a target="_blank" href="https://developer.hashicorp.com/terraform/language/backend/s3">https://developer.hashicorp.com/terraform/language/backend/s3</a></p>
<p><a target="_blank" href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html">https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html</a></p>
<p><a target="_blank" href="https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli">https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli</a></p>
<p><a target="_blank" href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html">https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html</a></p>
]]></content:encoded></item><item><title><![CDATA[Ansible. Securing your secrets.  Noob level.]]></title><description><![CDATA[So. You have you playbook in git and you want to keep some variables invisible to others. You probably should go for a ‘real’ password manager like Hashicorp Vault. Or pass (https://www.passwordstore.org/). Both have ready to use lookup plugins in An...]]></description><link>https://fossexperience.wawrzynczuk.com/ansible-securing-your-secrets-noob-level</link><guid isPermaLink="true">https://fossexperience.wawrzynczuk.com/ansible-securing-your-secrets-noob-level</guid><category><![CDATA[ansible]]></category><category><![CDATA[automation]]></category><category><![CDATA[secrets]]></category><category><![CDATA[Security]]></category><dc:creator><![CDATA[Maciej Wawrzyńczuk]]></dc:creator><pubDate>Sat, 27 Dec 2025 16:53:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1766851435414/ed6db8aa-7e74-4d4c-962d-fb6a1db6dc73.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>So. You have you playbook in git and you want to keep some variables invisible to others. You probably should go for a ‘real’ password manager like Hashicorp Vault. Or pass (<a target="_blank" href="https://www.passwordstore.org/">https://www.passwordstore.org/</a>). Both have ready to use lookup plugins in Ansible. But let’s start with something more… local. The ansible vault.</p>
<p>First, you have to remember your password. Store it in the <code>.vault_password</code> file in your home dir. Just remember to make it readable only for you.</p>
<pre><code class="lang-bash">chmod go-rwx ~/.vault_password
</code></pre>
<p>Now, tell Ansible to use the file. You can do it in ansible.cfg.</p>
<pre><code class="lang-ini"><span class="hljs-section">[defaults]</span>
<span class="hljs-attr">vault_password_file</span> = ~/.vault_password
</code></pre>
<p>We’re going to keep the secrets and private vars outside our repo. So - we’re going to create a kind of inventory which contain only variables.</p>
<pre><code class="lang-bash">mkdir -p ~/my_ansible_secrets/group_vars/all/
</code></pre>
<p>Then make ansible using it. We will add the new inventory to a config. Ansible don’t mind having multiple inventories. Now, our ansible.cfg looks like that</p>
<pre><code class="lang-ini"><span class="hljs-section">[defaults]</span>
<span class="hljs-attr">inventory</span> = testinv.yaml, ~/my_ansible_secrets
<span class="hljs-attr">vault_password_file</span> = ~/.ansible_password
</code></pre>
<p>Now’s time to encrypt.</p>
<pre><code class="lang-bash">ansible-vault encrypt_string -n my_secret <span class="hljs-string">'i wont tell'</span> &gt;&gt; ~/my_ansible_secrets/group_vars/all/secrets.yaml
</code></pre>
<p>Please look into the file. Adding minuses at the beginning of an YAML file won’t hurt.</p>
<pre><code class="lang-yaml"><span class="hljs-meta">---</span>
<span class="hljs-attr">my_secret:</span> <span class="hljs-type">!vault</span> <span class="hljs-string">|
          $ANSIBLE_VAULT;1.1;AES256
          66626630323438343166393164343162366634346362386163623036613333343465323635393839
          6638623963313135616434313165646232346132303338320a616137303230623436343031643436
          36613135363366303035653731616561333439383662383433346537643139653663396339346435
          6661313739643036360a393635313837346536633136303932316638386463343465376238373532
          6338</span>
</code></pre>
<p>Time to test:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766853961910/eb5558d6-d0a3-4af5-aa50-2b999503f3ed.png" alt class="image--center mx-auto" /></p>
<p>TADA! One more thing - let’s get rid of the ugly warning. Just create a a dummy inventory file.</p>
<pre><code class="lang-bash"> <span class="hljs-built_in">echo</span> <span class="hljs-string">'[all]'</span> &gt; ~/my_ansible_secrets/dummy.ini
</code></pre>
<p>That’s it. You really should use and real password manager… Anyway - you probably still will want to keep some private vars - like username - outside your official repo. You can use “vars only inventory” technique whenever you need.</p>
]]></content:encoded></item><item><title><![CDATA[The proper way of collecting data from multiple servers with Ansible]]></title><description><![CDATA[Ok. Your boos asked your to collect all installed packages on all servers… I already see you running a command on every servers, redirecting output to a temp file, fetching data, merging files… Wake up! It’s only nightmare. You can do it easy way.
Fo...]]></description><link>https://fossexperience.wawrzynczuk.com/the-proper-way-of-collecting-data-from-multiple-servers-with-ansible</link><guid isPermaLink="true">https://fossexperience.wawrzynczuk.com/the-proper-way-of-collecting-data-from-multiple-servers-with-ansible</guid><category><![CDATA[ansible]]></category><category><![CDATA[automation]]></category><dc:creator><![CDATA[Maciej Wawrzyńczuk]]></dc:creator><pubDate>Sun, 21 Dec 2025 15:02:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1766327657087/d83a3e13-f6e8-4465-b8de-a4fe8464ce26.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Ok. Your boos asked your to collect all installed packages on all servers… I already see you running a command on every servers, redirecting output to a temp file, fetching data, merging files… Wake up! It’s only nightmare. You can do it easy way.</p>
<p>For testing purpose - you can use the environment created there: <a target="_blank" href="https://fossexperience.hashnode.dev/ansible-hello-world">https://fossexperience.hashnode.dev/ansible-hello-world</a></p>
<p>So - let’s take a look at the code:</p>
<pre><code class="lang-yaml"><span class="hljs-meta">---</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">hosts:</span> <span class="hljs-string">testgroup</span>
  <span class="hljs-attr">tasks:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">gather</span>
      <span class="hljs-attr">command:</span> <span class="hljs-string">lsusb</span>
      <span class="hljs-attr">register:</span> <span class="hljs-string">result</span>

    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">make</span> <span class="hljs-string">report</span>
      <span class="hljs-attr">copy:</span>
        <span class="hljs-attr">dest:</span> <span class="hljs-string">report.txt</span>
        <span class="hljs-attr">content:</span> <span class="hljs-string">|
          {% for h in ansible_play_hosts %}
          {% for l in hostvars[h].result.stdout_lines %}
          {{h}};{{l | quote}}
          {% endfor %}
          {% endfor %}
</span>      <span class="hljs-attr">delegate_to:</span> <span class="hljs-string">localhost</span>
      <span class="hljs-attr">run_once:</span> <span class="hljs-literal">true</span>
</code></pre>
<p>In the '“gather” task we collect whatever we want. In this case - USB devices. And save it in the “result” variable.</p>
<p>The “make report” task is almost ‘normal’ copy. We learn two things:</p>
<ul>
<li><p>We can use <code>content</code> instead of <code>src</code>. I our case it is a Jinja2 template</p>
</li>
<li><p>We can access other hosts variables via <code>hostvars</code>.</p>
</li>
</ul>
<p>Let’s explain the template now.</p>
<ul>
<li><p>The outer loop. It just loops over all hosts in <code>ansible_play_hosts</code> built-in variable. And assigns it to <code>h</code>.</p>
</li>
<li><p>The inner loop. Loops over <code>result.stdout_lines</code>. Which contains the command output nicely split to lines.</p>
</li>
</ul>
<p>We’re delegating the “copy” command to the controller. And we run it only once.</p>
<p>One more thing. <strong>Always</strong> quote string you’re unsure of. It helps you to avoid real problems. Like crappy CSV with messed fields. Or code injection.</p>
<p>Good luck with with data collection. And wish good luck with data analysis to your boss. She/He’s gonna need it…</p>
]]></content:encoded></item><item><title><![CDATA[Ansible hello world]]></title><description><![CDATA[You want to start with ansible? Good luck. Bye… Oh no! I’m writing a blog. I must help you. Let’s start.
Install Ansible
I assume you have python3 / pip / virtualenv working on your system. We don’t want to mess up, so we’re going to use virtualenv.
...]]></description><link>https://fossexperience.wawrzynczuk.com/ansible-hello-world</link><guid isPermaLink="true">https://fossexperience.wawrzynczuk.com/ansible-hello-world</guid><category><![CDATA[ansible]]></category><category><![CDATA[Hello World]]></category><category><![CDATA[automation]]></category><dc:creator><![CDATA[Maciej Wawrzyńczuk]]></dc:creator><pubDate>Sat, 20 Dec 2025 21:55:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1766265126769/dbc92b1b-824b-4ba9-b0bb-6e7544136e87.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You want to start with ansible? Good luck. Bye… Oh no! I’m writing a blog. I must help you. Let’s start.</p>
<h1 id="heading-install-ansible">Install Ansible</h1>
<p>I assume you have python3 / pip / virtualenv working on your system. We don’t want to mess up, so we’re going to use virtualenv.</p>
<pre><code class="lang-bash">virtualenv venv
. venv/bin/activate
pip install ansible
</code></pre>
<p>That’s it. You can check it out typing <code>ansible --version</code>.</p>
<h1 id="heading-config-file">Config file.</h1>
<p>Create a config file. <code>ansible.cfg</code> in your working directory.</p>
<pre><code class="lang-ini"><span class="hljs-section">[defaults]</span>
<span class="hljs-attr">inventory</span> = testinv.yaml
<span class="hljs-attr">interpreter_python</span> = auto_silent
<span class="hljs-attr">nocows</span> = <span class="hljs-literal">true</span>
</code></pre>
<p>Let me explain.</p>
<p><code>[defaults]</code> - most of interesting settings goes to this section.</p>
<p><code>inventory</code> - our inventory. We’ll create it in the next step.</p>
<p><code>interpreter_python</code> - suppresses warning about python interpreter.</p>
<p><code>nocows</code> - suppresses using cowsay for displaying messages. Use at your own risk. ;)</p>
<h1 id="heading-inventory">Inventory</h1>
<p>Let’s create <code>testinv.yaml</code> file. You can also use ini format. I prefer yaml. Up to you.</p>
<pre><code class="lang-yaml"><span class="hljs-meta">---</span>
<span class="hljs-attr">all:</span>
  <span class="hljs-attr">children:</span>
    <span class="hljs-attr">testgroup:</span>
      <span class="hljs-attr">hosts:</span>
        <span class="hljs-attr">testhost:</span>
          <span class="hljs-attr">ansible_connection:</span> <span class="hljs-string">local</span>
</code></pre>
<p><code>all</code> - the root group name. Everything goes there.</p>
<p><code>children</code> - a group can have subgroups. List them under <code>children</code></p>
<p><code>testgoup</code> - our actual group name</p>
<p><code>hosts</code> - of course group can contain hosts…</p>
<p><code>testhosts</code> - a host. A kind of fake hosts for testing purpose. Normally the names must be resolvable in DNS. Or in <code>/etc/hosts</code>. Or be IP addresses.</p>
<p><code>ansible_connaction</code> - Describes way of connecting the host. <code>local</code> means localhost. Everything will be executed locally. It means the name from previous line doesn’t really matter.</p>
<h1 id="heading-the-first-playbook">The first playbook</h1>
<p>Let’s create the file - <code>hello.yaml'</code>. Yes. While using ansible - you’re and yaml developer.</p>
<pre><code class="lang-yaml"><span class="hljs-meta">---</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">hosts:</span> <span class="hljs-string">testgroup</span>
  <span class="hljs-attr">tasks:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">debug:</span>
        <span class="hljs-attr">msg:</span> <span class="hljs-string">Hello</span> <span class="hljs-string">World!</span>
</code></pre>
<p>Quite obvious. The playbook is executed on all boxes listed in the <code>testgoup</code> group. We’re going to display “Hello World!” message.</p>
<h1 id="heading-run-it">Run it</h1>
<p>The command is simple:</p>
<pre><code class="lang-bash">ansible-playbook hello.yaml
</code></pre>
<p>If you’re lucky and did not make any silly mistake - you’ll see:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766267466657/f8eea1ff-8723-4be9-a550-82945f95a21f.png" alt class="image--center mx-auto" /></p>
<p>That’s it. I hereby declare you and automation guru. Congratulations.</p>
]]></content:encoded></item><item><title><![CDATA[Let's start with an important question.]]></title><description><![CDATA[Do you have fortune|cowsay|lolcat in your profile? If no - I’m glad to help. First - you need to install everything.
sudo apt install fortune cowsay lolcat

Then - add it to your profile:
echo 'fortune | cowsay | lolcat' >> ~/.profile

That’s it. you...]]></description><link>https://fossexperience.wawrzynczuk.com/lets-start-with-an-important-question</link><guid isPermaLink="true">https://fossexperience.wawrzynczuk.com/lets-start-with-an-important-question</guid><category><![CDATA[Linux]]></category><category><![CDATA[Bash]]></category><category><![CDATA[fun]]></category><dc:creator><![CDATA[Maciej Wawrzyńczuk]]></dc:creator><pubDate>Fri, 19 Dec 2025 15:49:38 GMT</pubDate><content:encoded><![CDATA[<p>Do you have <code>fortune|cowsay|lolcat</code> in your profile? If no - I’m glad to help. First - you need to install everything.</p>
<pre><code class="lang-bash">sudo apt install fortune cowsay lolcat
</code></pre>
<p>Then - add it to your profile:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">echo</span> <span class="hljs-string">'fortune | cowsay | lolcat'</span> &gt;&gt; ~/.profile
</code></pre>
<p>That’s it. you can enjoy meaningful quotes on every login.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766158691323/1ac64f8b-9bc5-48ca-ae3a-b99ff4e309e8.png" alt class="image--center mx-auto" /></p>
]]></content:encoded></item></channel></rss>