<?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" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by CJ Hewett on Medium]]></title>
        <description><![CDATA[Stories by CJ Hewett on Medium]]></description>
        <link>https://medium.com/@cj-hewett?source=rss-1b88832fa9b8------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/2*7wNyagKHBJ8jvCpTwowogA.jpeg</url>
            <title>Stories by CJ Hewett on Medium</title>
            <link>https://medium.com/@cj-hewett?source=rss-1b88832fa9b8------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 03:59:17 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@cj-hewett/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Node.js is faster than Go]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://cj-hewett.medium.com/node-js-is-faster-than-go-5c2c72017829?source=rss-1b88832fa9b8------2"><img src="https://cdn-images-1.medium.com/max/1280/0*JgU0QtpZKcFzBE7e" width="1280"></a></p><p class="medium-feed-snippet">&#x2026;when you write bad code. A lesson in connection pooling, and not simply copying and pasting code you find on the internet.</p><p class="medium-feed-link"><a href="https://cj-hewett.medium.com/node-js-is-faster-than-go-5c2c72017829?source=rss-1b88832fa9b8------2">Continue reading on Medium »</a></p></div>]]></description>
            <link>https://cj-hewett.medium.com/node-js-is-faster-than-go-5c2c72017829?source=rss-1b88832fa9b8------2</link>
            <guid isPermaLink="false">https://medium.com/p/5c2c72017829</guid>
            <category><![CDATA[go]]></category>
            <category><![CDATA[connection-pool]]></category>
            <category><![CDATA[database]]></category>
            <dc:creator><![CDATA[CJ Hewett]]></dc:creator>
            <pubDate>Mon, 01 Apr 2024 05:42:53 GMT</pubDate>
            <atom:updated>2024-04-01T05:42:53.707Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Packaging Python requirements as an AWS Lambda Layer with Terraform]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://cj-hewett.medium.com/packaging-python-requirements-as-an-aws-lambda-layer-with-terraform-188f76db4e96?source=rss-1b88832fa9b8------2"><img src="https://cdn-images-1.medium.com/max/1200/0*_lS6GM654pF4obsB.png" width="1200"></a></p><p class="medium-feed-snippet">It can be easier to create a Lambda layer, and let Lambda functions consume the layer, than attempting to package libraries up with your&#x2026;</p><p class="medium-feed-link"><a href="https://cj-hewett.medium.com/packaging-python-requirements-as-an-aws-lambda-layer-with-terraform-188f76db4e96?source=rss-1b88832fa9b8------2">Continue reading on Medium »</a></p></div>]]></description>
            <link>https://cj-hewett.medium.com/packaging-python-requirements-as-an-aws-lambda-layer-with-terraform-188f76db4e96?source=rss-1b88832fa9b8------2</link>
            <guid isPermaLink="false">https://medium.com/p/188f76db4e96</guid>
            <category><![CDATA[python]]></category>
            <category><![CDATA[aws-lambda]]></category>
            <category><![CDATA[aws-lambda-layer]]></category>
            <category><![CDATA[terraform]]></category>
            <dc:creator><![CDATA[CJ Hewett]]></dc:creator>
            <pubDate>Tue, 14 Nov 2023 08:43:03 GMT</pubDate>
            <atom:updated>2023-11-17T19:20:01.718Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Self-Hosting a Terraform Provider Registry with AWS S3 and CloudFront]]></title>
            <link>https://cj-hewett.medium.com/self-hosting-a-terraform-provider-registry-with-aws-s3-and-cloudfront-a08e6373711d?source=rss-1b88832fa9b8------2</link>
            <guid isPermaLink="false">https://medium.com/p/a08e6373711d</guid>
            <category><![CDATA[terraform]]></category>
            <category><![CDATA[go]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[terraform-provider]]></category>
            <category><![CDATA[golang]]></category>
            <dc:creator><![CDATA[CJ Hewett]]></dc:creator>
            <pubDate>Tue, 07 Nov 2023 15:21:57 GMT</pubDate>
            <atom:updated>2023-11-07T15:21:57.847Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*sKBCdsSrBIWaCymZ.png" /></figure><p>Sometimes you need to host your own Terraform Provider, and want to do it privately, without the <a href="https://registry.terraform.io/">Terraform Registry</a>. This can be done via an S3 bucket and CloudFront, as the API just needs to respond with JSON, which can easily be done with some static JSON files. (This could most likely be done with Azure Blob Storage or GCP Cloud Storage Buckets as well).</p><p>We’ll go through:</p><ul><li>The Terraform to create the S3 bucket, CloudFront distributions, ACM Certificate and Route53 records.</li><li>Build an example Terraform Provider with GoReleaser.</li><li>Package our build to work as a registry and push it to AWS S3.</li><li>How to consume the Terraform Provider from the Private Terraform Registry.</li></ul><p>Code referenced in this article can be found in this GitHub repository. I started with a fork of an example provider and added what’s needed to create a registry with Terraform, and build + push the provider to it:</p><p><a href="https://github.com/cjh-cloud/terraform-provider-mock">GitHub - cjh-cloud/terraform-provider-mock: Boilerplate template for a custom terraform provider.</a></p><h4>Create a Git Tag</h4><p>Run git tag -f v0.0.1 in the root of the repository to create a Git tag on the current commit checked out that you would like to build the provider off of. GoReleaser will use the version of this tag to create a a build.</p><h4>GPG code signing</h4><p>Create a GPG key to used for code-signing by Go Releaser with: <br>gpg --gen-key <br>This command will ask you for a name for the key, an eMail address to associate with the key, and a password for the key.</p><p>Export the key to a file as “armored ascii”, we will need this later when packaging the provider and pushing it to S3.</p><p>gpg --armor --export --output key.txt &quot;{Key ID or email address}&quot;</p><p>Set an environment variable for the GPG Fingerprint in CLI, which will be part of the output when you create the key. This will be used by Go Releaser.</p><p>export GPG_FINGERPRINT=&quot;[~40 character fingerprint]&quot;</p><h4>.goreleaser.yaml</h4><p>Create a .goreleaser.yaml file in the root of your repository with the following content. This file uses the GPG_FINGERPRINT environment variable which is how GoReleaser knows which key to sign it with.</p><pre>---<br>builds:<br>- env:<br>  - CGO_ENABLED=0<br>  mod_timestamp: &quot;{{ .CommitTimestamp }}&quot;<br>  flags:<br>    - -trimpath<br>  ldflags:<br>    - &quot;-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}&quot;<br>  goos:<br>    - freebsd<br>    - windows<br>    - linux<br>    - darwin<br>  goarch:<br>    - amd64<br>    - &quot;386&quot;<br>    - arm<br>    - arm64<br>  ignore:<br>    - goos: darwin<br>      goarch: &quot;386&quot;<br>  binary: &quot;{{ .ProjectName }}_v{{ .Version }}&quot;<br>archives:<br>  - format: zip<br>    name_template: &quot;{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}&quot;<br>checksum:<br>  name_template: &quot;{{ .ProjectName }}_{{ .Version }}_SHA256SUMS&quot;<br>  algorithm: sha256<br>signs:<br>  - artifacts: checksum<br>    args:<br>      - &quot;--batch&quot;<br>      - &quot;--local-user&quot;<br>      - &quot;{{ .Env.GPG_FINGERPRINT }}&quot;<br>      - &quot;--output&quot;<br>      - &quot;${signature}&quot;<br>      - &quot;--detach-sign&quot;<br>      - &quot;${artifact}&quot;</pre><p>Build locally with goreleaser release --skip-publish , which will create a dist directory. Go Releaser will ask you for the GPG key password while building, so that it can do the code-signing step properly. <br><strong>Note:</strong> You don’t have to skip the publish step, set a GitHub token in the CLI for Go Releaser to also push the build files to GitHub as part of a release.</p><h4>Creating AWS Resources</h4><p>We’ll create the following AWS resources to host the Terraform provider:</p><ul><li>S3 bucket to store the provider build files.</li><li>CloudFront distribution to add a domain name.</li><li>ACM Certificate for SSL on the CF distribution.</li><li>Route53 records (assumed that host zone already exists).</li></ul><p>Change the local values to fit your needs, and run terraform init and terraform apply to create the resources. Make sure you have AWS credentials set either in a ~/.aws directory, or the current terminal session via environment variables. The bucket is set to public so that all files can be reached through the CloudFront distribution.</p><pre><br>provider &quot;aws&quot; {<br>  region = &quot;us-east-1&quot;<br>}<br><br>locals {<br>  name           = &quot;TF Provider&quot;<br>  domain_name    = &quot;cjscloud.city&quot;<br>  subdomain_name = &quot;tfp.${local.domain_name}&quot;<br>  environment    = &quot;default&quot;<br>}<br><br># S3 bucket<br>resource &quot;aws_s3_bucket&quot; &quot;provider&quot; {<br>  bucket = local.subdomain_name<br><br>  tags = {<br>    Name        = local.name<br>    Environment = local.environment<br>  }<br>}<br><br>resource &quot;aws_s3_bucket_public_access_block&quot; &quot;provider&quot; {<br>  bucket = aws_s3_bucket.provider.id<br><br>  block_public_acls       = false<br>  block_public_policy     = false<br>  ignore_public_acls      = false<br>  restrict_public_buckets = false<br>}<br><br>resource &quot;aws_s3_bucket_policy&quot; &quot;provider&quot; {<br>  bucket = aws_s3_bucket.provider.id<br>  policy = data.aws_iam_policy_document.provider.json<br>}<br><br>data &quot;aws_iam_policy_document&quot; &quot;provider&quot; {<br>  statement {<br>    principals {<br>      type        = &quot;*&quot;<br>      identifiers = [&quot;*&quot;]<br>    }<br><br>    actions = [<br>      &quot;s3:GetObject&quot;,<br>    ]<br><br>    resources = [<br>      &quot;${aws_s3_bucket.provider.arn}/*&quot;,<br>    ]<br>  }<br>}<br><br># ACM Certificate<br>resource &quot;aws_acm_certificate&quot; &quot;provider&quot; {<br>  domain_name       = local.subdomain_name<br>  validation_method = &quot;DNS&quot;<br><br>  tags = {<br>    Name        = local.name<br>    Environment = local.environment<br>  }<br><br>  lifecycle {<br>    create_before_destroy = true<br>  }<br>}<br><br># Route53 Records<br>data &quot;aws_route53_zone&quot; &quot;provider&quot; {<br>  name         = local.domain_name<br>  private_zone = false<br>}<br><br>resource &quot;aws_route53_record&quot; &quot;provider&quot; {<br>  for_each = {<br>    for dvo in aws_acm_certificate.provider.domain_validation_options : dvo.domain_name =&gt; {<br>      name   = dvo.resource_record_name<br>      record = dvo.resource_record_value<br>      type   = dvo.resource_record_type<br>    }<br>  }<br><br>  allow_overwrite = true<br>  name            = each.value.name<br>  records         = [each.value.record]<br>  ttl             = 60<br>  type            = each.value.type<br>  zone_id         = data.aws_route53_zone.provider.zone_id<br>}<br><br>resource &quot;aws_route53_record&quot; &quot;www&quot; {<br>  zone_id = data.aws_route53_zone.provider.zone_id<br>  name    = local.subdomain_name<br>  type    = &quot;CNAME&quot;<br>  ttl     = 300<br>  records = [aws_cloudfront_distribution.provider.domain_name]<br>}<br><br># Cert DNS validation, depended on by CloudFront<br>resource &quot;aws_acm_certificate_validation&quot; &quot;provider&quot; {<br>  certificate_arn         = aws_acm_certificate.provider.arn<br>  validation_record_fqdns = [for record in aws_route53_record.provider : record.fqdn]<br>}<br><br># Cloudfront<br>resource &quot;aws_cloudfront_distribution&quot; &quot;provider&quot; {<br>  origin {<br>    domain_name = aws_s3_bucket.provider.bucket_regional_domain_name<br>    origin_id   = aws_s3_bucket.provider.bucket_domain_name<br>  }<br><br>  enabled             = true<br>  is_ipv6_enabled     = true<br>  comment             = &quot;Terraform Provider Registry&quot;<br>  default_root_object = &quot;index.html&quot;<br><br>  aliases = [local.subdomain_name]<br><br>  default_cache_behavior {<br>    allowed_methods  = [&quot;GET&quot;, &quot;HEAD&quot;, &quot;OPTIONS&quot;]<br>    cached_methods   = [&quot;GET&quot;, &quot;HEAD&quot;]<br>    target_origin_id = aws_s3_bucket.provider.bucket_domain_name<br><br>    forwarded_values {<br>      query_string = false<br><br>      cookies {<br>        forward = &quot;none&quot;<br>      }<br>    }<br><br>    viewer_protocol_policy = &quot;allow-all&quot;<br>    min_ttl                = 0<br>    default_ttl            = 3600<br>    max_ttl                = 86400<br>  }<br><br>  price_class = &quot;PriceClass_100&quot;<br><br>  restrictions {<br>    geo_restriction {<br>      # restriction_type = &quot;whitelist&quot;<br>      # locations        = [&quot;NZ&quot;]<br>      restriction_type = &quot;none&quot;<br>    }<br>  }<br><br>  tags = {<br>    Name        = local.name<br>    Environment = local.environment<br>  }<br><br>  viewer_certificate {<br>    cloudfront_default_certificate = false<br>    acm_certificate_arn            = aws_acm_certificate.provider.arn<br>    ssl_support_method             = &quot;sni-only&quot;<br>  }<br>}</pre><h4>How to package the Terraform provider</h4><p>The following Go code can package the build in the dist/ directory and create the necessary files for a Terraform provider registry hosted as static files. In the example repo, this script is under a folder named tfpp/ and by default assumes there is a dist/ folder and key.txt file in the parent directory, but different paths can be provided via command line flags.</p><p>An example of calling this would be like:</p><pre>go run main.go \<br>  -ns=cjh-cloud \<br>  -d=tfp.cjscloud.city \<br>  -gf=$GPG_FINGERPRINT \<br>  -v=0.0.1</pre><h4>main.go</h4><p>Here’s the Go script that will package a Terraform provider build to act as a Terraform Registry, hosted statically. This is in the GitHub repo mentioned at the start as in the tfpp/ (Terraform Provider Packager) directory.</p><pre>package main<br><br>import (<br> &quot;bufio&quot;<br> &quot;flag&quot;<br> &quot;fmt&quot;<br> &quot;io&quot;<br> &quot;log&quot;<br> &quot;os&quot;<br> &quot;strings&quot;<br>)<br><br>func main() {<br> log.Println(&quot;📦 Packaging Terraform Provider for private registry...&quot;)<br><br> namespace := flag.String(&quot;ns&quot;, &quot;&quot;, &quot;Namespace for the Terraform registry.&quot;) // No default<br> domain := flag.String(&quot;d&quot;, &quot;&quot;, &quot;Private Terraform registry domain.&quot;) // No default<br> providerName := flag.String(&quot;p&quot;, &quot;mock&quot;, &quot;Name of the Terraform provider.&quot;)<br> distPath := flag.String(&quot;dp&quot;, &quot;../dist&quot;, &quot;Path to Go Releaser build files.&quot;)<br> repoName := flag.String(&quot;r&quot;, &quot;terraform-provider-mock&quot;, &quot;Name of the provider repository used in Go Releaser build name.&quot;)<br> version := flag.String(&quot;v&quot;, &quot;0.0.1&quot;, &quot;Semantic version of build.&quot;)<br> gpgFingerprint := flag.String(&quot;gf&quot;, &quot;&quot;, &quot;GPG Fingerprint of key used by Go Releaser&quot;) // No default<br> gpgPubKeyFile := flag.String(&quot;gk&quot;, &quot;../key.txt&quot;, &quot;Path to GPG Public Key in ASCII Armor format.&quot;)<br> flag.Parse()<br><br> *distPath = *distPath + &quot;/&quot; // If the path already ends in &quot;/&quot;, it shouldn&#39;t matter<br><br> // Create release dir - only the contents of this need to be uploaded to S3<br> err := createDir(&quot;release&quot;)<br> if err != nil {<br>  log.Printf(&quot;Error creating &#39;release&#39; dir: %s&quot;, err)<br> }<br><br> // Create .wellKnown directory and terraform.json file<br> err = wellKnown()<br> if err != nil {<br>  log.Printf(&quot;Error creating &#39;.wellKnown&#39; dir: %s&quot;, err)<br> }<br><br> // Create v1 directory<br> err = provider(*namespace, *providerName, *distPath, *repoName, *version, *gpgFingerprint, *gpgPubKeyFile, *domain)<br> if err != nil {<br>  log.Printf(&quot;Error creating &#39;v1&#39; dir: %s&quot;, err)<br> }<br><br> log.Println(&quot;📦 Packaged Terraform Provider for private registry.&quot;)<br>}<br><br>// This establishes the &quot;API&quot; as a TF provider by responding with the correct JSON payload, by using static files<br>func wellKnown() (error) {<br> log.Println(&quot;* Creating .well-known directory&quot;)<br><br> err := createDir(&quot;release/.well-known&quot;)<br> if err != nil {<br>  return err<br> }<br><br> terraformJson := []byte(`{&quot;providers.v1&quot;: &quot;/v1/providers/&quot;}`)<br><br> log.Println(&quot;  - Writing to .well-known/terraform.json file&quot;)<br> err = writeFile(&quot;release/.well-known/terraform.json&quot;, terraformJson)<br> if err != nil {<br>  return err<br> }<br><br> return nil<br>}<br><br>// provider is the Terraform name<br>// repoName is the Repository name<br>func provider(namespace, provider, distPath, repoName, version, gpgFingerprint, gpgPubKeyFile, domain string) (error) {<br> // Path to semantic version dir<br> versionPath := providerDirs(namespace, provider, version)<br><br> // Files to create under v1/providers/[namespace]/[provider_name]<br> createVersionsFile(namespace, provider, distPath, repoName, version) // Creates version file one above download, which is why downloadPath isn&#39;t used<br><br> // Files/Directories to create under v1/providers/[namespace]/[provider_name]/[version]<br> copyShaFiles(versionPath, distPath, repoName, version)<br> downloadPath, err :=  createDownloadsDir(versionPath)<br> if err != nil {<br>  return err<br> }<br><br> // Create darwin, freebsd, linux, windows dirs<br> createTargetDirs(*downloadPath)<br><br> // Copy all zips<br> copyBuildZips(*downloadPath, distPath, repoName, version)<br><br> // Create all individual files for build targets and each architecture for the build targets<br> createArchitectureFiles(namespace, provider, distPath, repoName, version, gpgFingerprint, gpgPubKeyFile, domain)<br><br> return nil<br>}<br><br>// Create the directories with a path format v1/providers/[namespace]/[provider_name]/[version]<br>func providerDirs(namespace, repoName, version string) string {<br> log.Println(&quot;* Creating release/v1/providers/[namespace]/[repo]/[version] directories&quot;)<br><br> providerPathArr := [6]string{&quot;release&quot;, &quot;v1&quot;, &quot;providers&quot;, namespace, repoName, version}<br><br> var currentPath string<br> for _, v := range providerPathArr {<br>  currentPath = currentPath + v + &quot;/&quot;<br>  createDir(currentPath)<br> }<br><br> return currentPath<br>}<br><br>// Create the versions file under v1/providers/[namespace]/[provider_name]<br>func createVersionsFile(namespace, provider, distPath, repoName, version string) (error) {<br> log.Println(&quot;* Writing to release/v1/providers/[namespace]/[repo]/versions file&quot;)<br><br> versionPath := fmt.Sprintf(&quot;release/v1/providers/%s/%s/versions&quot;, namespace, provider)<br><br> shaSumContents, err := getShaSumContents(distPath, repoName, version)<br> if err != nil {<br>  return err<br> }<br><br> // Build the versions file...<br> platforms := &quot;&quot;<br> for _, line := range shaSumContents {<br>  fileName := line[1] // zip file name<br><br>  // get os and arch from filename<br>  removeFileExtension := strings.Split(fileName, &quot;.zip&quot;)<br>  fileNameSplit := strings.Split(removeFileExtension[0], &quot;_&quot;)<br><br>  // Get build target and architecture from the zip file name<br>  target := fileNameSplit[2]<br>  arch := fileNameSplit[3]<br><br>  platforms += &quot;{&quot;<br>    platforms += fmt.Sprintf(`&quot;os&quot;: &quot;%s&quot;,`, target)<br>    platforms += fmt.Sprintf(`&quot;arch&quot;: &quot;%s&quot;`, arch)<br>  platforms += &quot;}&quot;<br>  platforms += &quot;,&quot;<br> }<br> platforms = strings.TrimRight(platforms, &quot;,&quot;) // remove trailing comma, json does not allow<br><br> var versions = []byte(fmt.Sprintf(`<br>{<br>  &quot;versions&quot;: [<br>    {<br>      &quot;version&quot;: &quot;%s&quot;,<br>      &quot;protocols&quot;: [<br>        &quot;4.0&quot;,<br>        &quot;5.1&quot;<br>      ],<br>      &quot;platform&quot;: [<br>        %s<br>      ]<br>    }<br>  ]<br>}<br>`, version, platforms))<br><br> writeFile(versionPath, versions)<br><br> return nil<br>}<br><br>func copyShaFiles(destPath, srcPath, repoName, version string) {<br> log.Printf(&quot;* Copying SHA files in %s directory&quot;, srcPath)<br><br> // Copy files from srcPath <br> shaSum := repoName + &quot;_&quot; + version + &quot;_SHA256SUMS&quot;<br> shaSumPath := srcPath + &quot;/&quot; + shaSum<br><br> // _SHA256SUMS file<br> _, err := copyFile(shaSumPath, destPath + shaSum)<br> if err != nil {<br>  log.Println(err)<br> }<br><br> // _SHA256SUMS.sig file<br> _, err = copyFile(shaSumPath + &quot;.sig&quot;, destPath + shaSum + &quot;.sig&quot;)<br> if err != nil {<br>  log.Println(err)<br> }<br>}<br><br>func createDownloadsDir(destPath string) (*string, error) {<br> log.Printf(&quot;* Creating download/ in %s directory&quot;, destPath)<br><br> downloadPath := destPath + &quot;download/&quot;<br> <br> err := createDir(downloadPath)<br> if err != nil {<br>  return nil, err<br> }<br><br> return &amp;downloadPath, nil<br>}<br><br>func createTargetDirs(destPath string) (error) {<br> log.Printf(&quot;* Creating target dirs in %s directory&quot;, destPath)<br><br> targets := [4]string{&quot;darwin&quot;, &quot;freebsd&quot;, &quot;linux&quot;, &quot;windows&quot;}<br><br> for _, v := range targets {<br>  err := createDir(destPath + v)<br>  if err != nil {<br>   return err<br>  }<br> }<br><br> return nil<br>}<br><br>func copyBuildZips(destPath, distPath, repoName, version string) (error) {<br> log.Println(&quot;* Copying build zips&quot;)<br><br> shaSumContents, err := getShaSumContents(distPath, repoName, version)<br> if err != nil {<br>  return err<br> }<br><br> // Loop through and copy each<br> for _, v := range shaSumContents {<br>  zipName := v[1]<br>  zipSrcPath := distPath + zipName<br>  zipDestPath := destPath + zipName<br><br>  log.Printf(&quot;  - Zip Source: %s&quot;, zipSrcPath)<br>  log.Printf(&quot;   - Zip Dest:  %s&quot;, zipDestPath)<br>  <br>  // Copy the zip<br>  _, err := copyFile(zipSrcPath, zipDestPath)<br>  if err != nil {<br>   return err<br>  }<br> }<br><br> return nil<br>}<br><br>func getShaSumContents(distPath, repoName, version string) ([][]string, error) {<br> shaSumFileName := repoName + &quot;_&quot; + version + &quot;_SHA256SUMS&quot;<br> shaSumPath := distPath + &quot;/&quot; + shaSumFileName<br><br> shaSumLine, err := readFile(shaSumPath)<br> if err != nil {<br>  return nil, err<br> }<br><br> buildsAndShaSums := [][]string{}<br><br> for _, line := range shaSumLine {<br>  lineSplit := strings.Split(line, &quot;  &quot;)<br><br>  row := []string{lineSplit[0], lineSplit[1]}<br>  buildsAndShaSums = append(buildsAndShaSums, row)<br> }<br><br> // log.Println(buildsAndShaSums)<br><br> return buildsAndShaSums, nil<br>}<br><br>// Create architecture files for each build target<br>func createArchitectureFiles(namespace, provider, distPath, repoName, version, gpgFingerprint, gpgPubKeyFile, domain string) (error) {<br> log.Println(&quot;* Creating architecure files in target directories&quot;)<br><br> // filename = terraform-provider-[provider]_0.0.1_darwin_amd64.zip - provider_name + version + target + architecture + .zip<br> prefix := fmt.Sprintf(&quot;v1/providers/%s/%s/%s/&quot;, namespace, provider, version)<br> pathPrefix := fmt.Sprintf(&quot;release/%s&quot;, prefix)<br> urlPrefix := fmt.Sprintf(&quot;https://%s/%s&quot;, domain, prefix)<br><br> // download url = https://example.com/v1/providers/namespace/provider/0.0.1/download/terraform-provider_0.0.1_darwin_amd64.zip<br> downloadUrlPrefix := urlPrefix + &quot;download/&quot;<br> downloadPathPrefix := pathPrefix + &quot;download/&quot;<br><br> // shasums url = https://example.com/v1/providers/namespace/provider/0.0.1/terraform-provider_0.0.1_SHA256SUMS<br> shasumsUrl := urlPrefix + fmt.Sprintf(&quot;%s_%s_SHA256SUMS&quot;, repoName, version)<br> // shasums_signature_url = https://example.com/v1/providers/namespace/provider/0.0.1/terraform-provider_0.0.1_SHA256SUMS.sig<br> shasumsSigUrl := shasumsUrl + &quot;.sig&quot;<br><br> shaSumContents, err := getShaSumContents(distPath, repoName, version)<br> if err != nil {<br>  return err<br> }<br><br> // Get contents of GPG key<br> gpgFile, err := readFile(gpgPubKeyFile)<br> if err != nil {<br>  log.Printf(&quot;Error reading &#39;%s&#39; file: %s&quot;, gpgPubKeyFile, err)<br> }<br><br> // loop through every line and stick with \\n<br> gpgAsciiPub := &quot;&quot;<br> for _, line := range gpgFile {<br>  gpgAsciiPub = gpgAsciiPub + line + &quot;\\n&quot;<br> }<br> // log.Println(gpgAsciiPub)<br><br> for _, line := range shaSumContents {<br>  shasum := line[0] // shasum of the zip<br>  fileName := line[1] // zip file name<br><br>  downloadUrl := downloadUrlPrefix + fileName<br><br>  // get os and arch from filename<br>  removeFileExtension := strings.Split(fileName, &quot;.zip&quot;)<br>  fileNameSplit := strings.Split(removeFileExtension[0], &quot;_&quot;)<br><br>  // Get build target and architecture from the zip file name<br>  target := fileNameSplit[2]<br>  arch := fileNameSplit[3]<br>  <br>  // build filepath<br>  archFileName := downloadPathPrefix + target + &quot;/&quot; + arch <br><br>  var architectureTemplate = []byte(fmt.Sprintf(`<br>{<br>  &quot;protocols&quot;: [<br>    &quot;4.0&quot;,<br>    &quot;5.1&quot;<br>  ],<br>  &quot;os&quot;: &quot;%s&quot;,<br>  &quot;arch&quot;: &quot;%s&quot;,<br>  &quot;filename&quot;: &quot;%s&quot;,<br>  &quot;download_url&quot;: &quot;%s&quot;,<br>  &quot;shasums_url&quot;: &quot;%s&quot;,<br>  &quot;shasums_signature_url&quot;: &quot;%s&quot;,<br>  &quot;shasum&quot;: &quot;%s&quot;,<br>  &quot;signing_keys&quot;: {<br>    &quot;gpg_public_keys&quot;: [<br>      {<br>        &quot;key_id&quot;: &quot;%s&quot;,<br>        &quot;ascii_armor&quot;: &quot;%s&quot;,<br>        &quot;trust_signature&quot;: &quot;&quot;,<br>        &quot;source&quot;: &quot;&quot;,<br>        &quot;source_url&quot;: &quot;&quot;<br>      }<br>    ]<br>  }<br>}<br>`, target, arch, fileName, downloadUrl, shasumsUrl, shasumsSigUrl, shasum, gpgFingerprint, gpgAsciiPub))<br><br>  log.Printf(&quot;  - Arch file: %s&quot;, archFileName)<br><br>  err := writeFile(archFileName, architectureTemplate)<br>  if err != nil {<br>   return err<br>  }<br> }<br><br> return nil<br>}<br><br>func createDir(path string) (error) {<br> err := os.Mkdir(path, os.ModePerm)<br> return err<br>}<br><br>func copyFile(src, dst string) (int64, error) {<br> sourceFileStat, err := os.Stat(src)<br> if err != nil {<br>  return 0, err<br> }<br><br> if !sourceFileStat.Mode().IsRegular() {<br>  return 0, fmt.Errorf(&quot;%s is not a regular file&quot;, src)<br> }<br><br> source, err := os.Open(src)<br> if err != nil {<br>  return 0, err<br> }<br> defer source.Close()<br><br> destination, err := os.Create(dst)<br> if err != nil {<br>  return 0, err<br> }<br> defer destination.Close()<br> nBytes, err := io.Copy(destination, source)<br> return nBytes, err<br>}<br><br>func readFile(filePath string) ([]string, error) {<br> readFile, err := os.Open(filePath)<br><br> if err != nil {<br>  return nil, err<br> }<br> fileScanner := bufio.NewScanner(readFile)<br> fileScanner.Split(bufio.ScanLines)<br> var fileLines []string<br><br> for fileScanner.Scan() {<br>  fileLines = append(fileLines, fileScanner.Text())<br> }<br><br> readFile.Close()<br><br> return fileLines, nil<br>}<br><br>func writeFile(fileName string, fileContents []byte) (error) {<br> err := os.WriteFile(fileName, fileContents, 0644)<br> return err<br>}</pre><p>This will create a release/ directory, which can be pushed to the S3 bucket with something like aws s3 sync release s3://tfp.cjscloud.city. The directory structure of the release will look something like in the following image.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/942/1*uhpMjJGi9ZCC9xD_zuqQpg.png" /><figcaption>Directory Tree</figcaption></figure><h4>Using the provider</h4><p>Define the provider and set the source and version under required_providers, as well as defining the provider object.</p><pre>terraform {<br>  required_providers {<br>    mock = {<br>      source  = &quot;tfp.cjscloud.city/cjh-cloud/mock&quot;<br>      version = &quot;0.0.1&quot;<br>    }<br>  }<br>}<br><br>provider &quot;mock&quot; {<br>  foo = &quot;test_foo_value&quot;<br>}<br><br>resource &quot;mock_example&quot; &quot;testing&quot; {<br>  not_computed_required = &quot;some value&quot;<br><br>  dynamic &quot;foo&quot; {<br>    for_each = [{ number = 1 }, { number = 2 }, { number = 3 }]<br>    content {<br>      bar {<br>        number = foo.value.number<br>      }<br>    }<br>  }<br>  /*<br>   * The above is equivalent to:<br>   *<br>   * foo {<br>   *   bar {<br>   *     number = 1<br>   *   }<br>   * }<br>   * foo {<br>   *   bar {<br>   *     number = 2<br>   *   }<br>   * }<br>   * foo {<br>   *   bar {<br>   *     number = 3<br>   *   }<br>   * }<br>  */<br><br>  dynamic &quot;baz&quot; {<br>    // The variable inside the for_each block doesn&#39;t have to be the same as <br>    // what you&#39;re assigning the value to.<br>    for_each = [{ something = &quot;x&quot; }, { something = &quot;y&quot; }, { something = &quot;z&quot; }]<br>    content {<br>      qux = baz.value.something<br>    }<br>  }<br>  /*<br>   * The above is equivalent to:<br>   *<br>   * baz {<br>   *   qux = &quot;x&quot;<br>   * }<br>   * baz {<br>   *   qux = &quot;y&quot;<br>   * }<br>   * baz {<br>   *   qux = &quot;z&quot;<br>   * }<br>  */<br><br>  some_list = [&quot;a&quot;, &quot;b&quot;, &quot;c&quot;]<br>}<br><br>output &quot;last_updated&quot; {<br>  value = mock_example.testing.last_updated<br>}</pre><p>Run terraform init to download the provider from the private registry, which should successfully initialise. Then run terraform apply to create a resource using the provider.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*50IMUnyxMX3TPJ8li0lPYg.png" /><figcaption>Successfully “terraform init”</figcaption></figure><h4>Cleanup</h4><p>Empty the S3 bucket of files, then run terraform destroy to remove all the AWS resources when tearing down the private registry.</p><p>If you found this useful, please consider following!<br>Cheers,</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a08e6373711d" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Using Terraform workspaces with an AWS S3 backend]]></title>
            <link>https://cj-hewett.medium.com/using-terraform-workspaces-with-an-aws-s3-backend-a4250529b5fb?source=rss-1b88832fa9b8------2</link>
            <guid isPermaLink="false">https://medium.com/p/a4250529b5fb</guid>
            <category><![CDATA[aws-s3]]></category>
            <category><![CDATA[iac]]></category>
            <category><![CDATA[infrastructure-as-code]]></category>
            <category><![CDATA[terraform-workspace]]></category>
            <category><![CDATA[terraform]]></category>
            <dc:creator><![CDATA[CJ Hewett]]></dc:creator>
            <pubDate>Mon, 30 Oct 2023 22:52:53 GMT</pubDate>
            <atom:updated>2023-10-30T22:52:53.573Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*uWeE5RFOgpkUHwkw.png" /></figure><p>Terraform workspaces simplify having multiple environments with the same backend. They also improve developer experience by making it easier to create new workspaces, and switch between them using CLI commands.</p><p>Code for this article can be found here:</p><p><a href="https://github.com/cjh-cloud/lambda-chain-tf/tree/main">GitHub - cjh-cloud/lambda-chain-tf: Example of a multiple AWS Lambdas, with Terraform Workspace S3 backend</a></p><h4>Set up S3 backend resources</h4><p>An S3 bucket will be created to store the Terraform state files for every workspace. We will also enable object versioning on the bucket, so that we can retrieve past versions of the Terraform state if we need to role back. Server side encryption is turned on for security.</p><p>A DynamoDB table is also created to allow the Terraform backend to handle state locking, to protect the state from concurrent plans or applies, allowing a team to potentially work together on this, as well as CI integration. Deletion protection is also enabled so that the table does not get accidentally deleted. Remember to set this to false and do an apply when cleaning everything up afterwards, otherwise Terraform won’t be able to delete the table.</p><p>The count to only create if the current selected workspace is default, is not necessary, only if you’re creating other workspaces in the same directory (which we won’t be doing this time around), as we only need one backend.</p><h4><strong>backend_res.tf</strong></h4><pre>resource &quot;aws_s3_bucket&quot; &quot;backend&quot; {<br>  count = terraform.workspace == &quot;default&quot; ? 1 : 0<br><br>  bucket_prefix = &quot;terraform-backend&quot;<br><br>  tags = {<br>    Name        = &quot;Terraform Backend&quot;<br>    Environment = terraform.workspace<br>  }<br>}<br><br>output &quot;backend_bucket_name&quot; {<br>  value = aws_s3_bucket.backend.0.id<br>}<br><br>resource &quot;aws_s3_bucket_versioning&quot; &quot;backend&quot; {<br>  count = terraform.workspace == &quot;default&quot; ? 1 : 0<br><br>  bucket = aws_s3_bucket.backend.0.id<br>  versioning_configuration {<br>    status = &quot;Enabled&quot;<br>  }<br>}<br><br>resource &quot;aws_s3_bucket_server_side_encryption_configuration&quot; &quot;backend&quot; {<br>  count = terraform.workspace == &quot;default&quot; ? 1 : 0<br><br>  bucket = aws_s3_bucket.backend.0.id<br><br>  rule {<br>    apply_server_side_encryption_by_default {<br>      sse_algorithm = &quot;AES256&quot;<br>    }<br>  }<br>}<br><br>resource &quot;aws_dynamodb_table&quot; &quot;terraform_lock&quot; {<br>  count = terraform.workspace == &quot;default&quot; ? 1 : 0<br><br>  name                        = &quot;terraform_state&quot;<br>  deletion_protection_enabled = true<br>  read_capacity               = 5<br>  write_capacity              = 5<br>  hash_key                    = &quot;LockID&quot;<br>  attribute {<br>    name = &quot;LockID&quot;<br>    type = &quot;S&quot;<br>  }<br>  tags = {<br>    Name        = &quot;Terraform Backend&quot;<br>    Environment = terraform.workspace<br>  }<br>}</pre><p>Run a terraform apply to create the resources. After the resources are created, there should be an output section with the name of the S3 bucket created, this will be used in the backend.</p><h4>Migrate the state to the S3 remote backend</h4><p>replace the bucket value with the output from above.<br>Change region to suit your needs, unfortunately variables are not accepted here, so we can’t use var.region .</p><h4><strong>backend.tf</strong></h4><pre>terraform {<br><br>  # Comment this out when initialising resources.<br>  backend &quot;s3&quot; {<br>    region         = &quot;ap-southeast-2&quot;<br>    bucket         = &quot;terraform-backend20231013052848766400000001&quot;<br>    key            = &quot;services/tf-workspace-ex/default.tfstate&quot;<br>    dynamodb_table = &quot;terraform_state&quot;<br>  }<br><br>  required_providers {<br>    aws = {<br>      source  = &quot;hashicorp/aws&quot;<br>      version = &quot;5.11.0&quot;<br>    }<br>  }<br>}<br><br>provider &quot;aws&quot; {<br>  region = var.region<br>}<br><br>variable &quot;region&quot; {<br>  description = &quot;AWS Region&quot;<br>  default     = &quot;ap-southeast-2&quot;<br>  type        = string<br>}</pre><p>Once the above backend.tf file is created, run terraform init . Since the current state for the S3 and DynamoDB resources is in a local terraform.tfstate file, it will ask if you would like to copy state to the remote backend, select <strong>yes</strong>.</p><p>✅ The remote backend is finished, we can now start creating environments and Terraform code for our project resources.</p><h4>Create a project that uses the backend and Terraform Workspaces</h4><p>Workspaces are a great way to define infrastructure once, and create multiple environments off of it without needing to change the configuration in the backend block.</p><p>Make a project directory in the same directory as the existing Terraform backend resources.</p><p><strong>Note:</strong> You don’t need to create a project directory, you could do everything within the current directory. If you go down this route, I recommend not creating resources within the default workspace, and using that only for the backend state resources.</p><p>Move into the project dir with cd project and add some new Terraform resources here for your project.</p><p>Create another backend.tf file too, the only thing slightly different to the previous one above is the key, to make sure the state file in the bucket has a different name:</p><pre>terraform {<br><br>  backend &quot;s3&quot; {<br>    region         = &quot;ap-southeast-2&quot;<br>    bucket         = &quot;terraform-backend20231013052848766400000001&quot;<br>    key            = &quot;services/tf-workspace-ex/project.tfstate&quot;<br>    dynamodb_table = &quot;terraform_state&quot;<br>  }<br><br>  required_providers {<br>    aws = {<br>      source  = &quot;hashicorp/aws&quot;<br>      version = &quot;5.11.0&quot;<br>    }<br>  }<br>}<br><br>provider &quot;aws&quot; {<br>  region = var.region<br>}</pre><p>Run terraform init and then create a new workspace for a development environment with terraform workspace new dev .</p><p>Apply the terraform with terraform apply -var-file=./vars/dev.tfvars . <strong>Note:</strong> I’m using var files for each environment, so leave that part off of the command if you aren’t.</p><p>Create a new workspace for a test environment terraform workspace new test .</p><p>Apply the terraform for the test environment terraform apply -var-file=./vars/test.tfvars .</p><p>View workspaces with terraform workspace list .</p><p>Select dev workspace again with terraform workspace select dev .</p><p>You’ve now created resources for a Dev and Test environment, with the Terraform state stored remotely in an S3 bucket.</p><h4>Clean up</h4><p>Run terraform destroy -var-file=./vars/dev.tfvars and switch to Test with terraform workspace select test and run terraform destroy -var-file=./vars/test.tfvars. This cleans up all the resources in the project directory.</p><p>Move up one directory to where the backend resources are and disable deletion protection on the DynamoDB table if need be. (set this to false in the backend_res.tf file and do an apply ). Manually delete the contents in the S3 bucket from the AWS console, as Terraform cannot destroy non-empty buckets.</p><p>Run terraform destroy . Everything should now be cleaned up 👍.</p><p>I hope you found that useful!<br>Thanks for reading, cheers 🤙</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a4250529b5fb" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Automate versioning of Terraform with GitHub actions]]></title>
            <link>https://cj-hewett.medium.com/automate-versioning-of-terraform-with-github-actions-4a9f28d965c?source=rss-1b88832fa9b8------2</link>
            <guid isPermaLink="false">https://medium.com/p/4a9f28d965c</guid>
            <category><![CDATA[github-actions]]></category>
            <category><![CDATA[terraform]]></category>
            <category><![CDATA[semantic-versioning]]></category>
            <dc:creator><![CDATA[CJ Hewett]]></dc:creator>
            <pubDate>Thu, 19 Oct 2023 10:05:06 GMT</pubDate>
            <atom:updated>2023-10-22T04:56:02.488Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*xnaeN-e1nL2KVhxB.png" /></figure><p>Automate versioning of Terraform in a GitHub repo with the use of GitHub Actions and <a href="https://github.com/semantic-release/semantic-release">Semantic Release</a>. GitHub Actions will automate the process of running Semantic Release on commits to a chosen branch, and Semantic release will create a Git Tag, and a GitHub Release with release notes about changes in the added commits.</p><p>This action will checkout your repo, setup node, install semantic release (this could be cached using NPM cache, to speed up future runs), run sematic-release with a GitHub token, so that the action has permissions to create a release and tag for the repo.</p><p>Code included in this post can be found here:</p><p><a href="https://github.com/cjh-cloud/lambda-chain-tf/tree/main">GitHub - cjh-cloud/lambda-chain-tf: Example of a multiple AWS Lambdas, with Terraform Workspace S3 backend</a></p><h4>.github/workflows/release.yaml</h4><p>This file defines the GitHub Action:</p><ul><li>Runs on push to prod branch, change this to fit.</li><li>Sets permissions to be able to create releases</li><li>Checkout repo, setup node, install Semantic Release and related plugins, run Semantic Release with GitHub token.</li><li>GITHUB_TOKEN — you don’t need to set up the secret, that’s automagical, and handles by GitHub Actions. The token is needed so that Semantic Release is able to create and push a Git Tag, as well as create the GitHub Release.</li></ul><pre>name: GitHub Release<br><br>on:<br>  push:<br>    branches:<br>      - prod<br><br># A workflow run is made up of one or more jobs that can run sequentially or in parallel<br>jobs:<br>  # This workflow contains a single job called &quot;build&quot;<br>  release:<br>    # The type of runner that the job will run on<br>    runs-on: ubuntu-latest<br><br>    permissions:<br>      contents: write<br>      issues: read<br><br>    # Steps represent a sequence of tasks that will be executed as part of the job<br>    steps:<br>      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it<br>      - uses: actions/checkout@v3<br><br>      - name: Setup Node.js environment<br>        uses: actions/setup-node@v3.7.0<br>        with:<br>          # Version Spec of the version to use. Examples: 12.x, 10.15.1, &gt;=10.15.0.<br>          node-version: 18.x # optional<br><br>      - name: Install Semantic Release and plugins<br>        run: |<br>          npm install semantic-release @semantic-release/exec @semantic-release/changelog @semantic-release/github<br><br>      - name: Create Release<br>        run: npx semantic-release<br>        env: <br>          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}</pre><p>Next it our config for semantic release, so that it can work out how to bump the version, based on the commits being merged into the branch.</p><h4>.releaserc.yaml</h4><p>Important parts are:</p><ul><li>branches — Here is is set to prod , but change this to suit your needs.</li><li>analyzeCommits — decides how to bump semantic version based on commit messages in the merging branch. E.g. commits beginning with feat: are considered a minor release, and fix: is considered a patch.</li><li>The exec command — outputs the new version to a file</li></ul><pre>---<br>branches:<br>  - prod<br>defaultBranch: &quot;prod&quot;<br>preset: &quot;angular&quot;<br>tagFormat: &quot;${version}&quot;<br>plugins:<br>  - &quot;@semantic-release/commit-analyzer&quot;<br>  - &quot;@semantic-release/release-notes-generator&quot;<br>  - &quot;@semantic-release/changelog&quot;<br>  - &quot;@semantic-release/github&quot;<br>  - &quot;@semantic-release/exec&quot;<br><br>verifyConditions:<br>  - &quot;@semantic-release/github&quot;<br><br>analyzeCommits:<br>  - path: &quot;@semantic-release/commit-analyzer&quot;<br>    releaseRules:<br>    - type: &quot;feat&quot;<br>      release: &quot;minor&quot;<br>    - type: &quot;fix&quot;<br>      release: &quot;patch&quot;<br>    - type: &quot;breaking&quot;<br>      release: &quot;major&quot;<br>    - type: &quot;refactor&quot;<br>      release: &quot;patch&quot;<br>    - type: &quot;chore&quot;<br>      release: &quot;patch&quot;<br><br>generateNotes:<br>  - path: &quot;@semantic-release/release-notes-generator&quot;<br>    writerOpts:<br>      groupBy: &quot;type&quot;<br>      commitGroupsSort:<br>        - &quot;breaking&quot;<br>        - &quot;feat&quot;<br>        - &quot;fix&quot;<br>        - &quot;chore&quot;<br>      commitsSort: &quot;header&quot;<br>    types:<br>    - type: &quot;feat&quot;<br>    - section: &quot;Features&quot;<br>    - type: &quot;fix&quot;<br>    - section: &quot;Bug Fixes&quot;<br>    - type: &quot;breaking&quot;<br>    - section: &quot;Breaking Changes&quot;<br>    - type: &quot;refactor&quot;<br>    - hidden: true<br>    - type: &quot;chore&quot;<br>    - section: &quot;Chore&quot;<br>    - type: &quot;docs&quot;<br>    - hidden: true<br>    - type: &quot;doc&quot;<br>    - hidden: true<br>    - type: &quot;style&quot;<br>    - hidden: true<br>    - type: &quot;perf&quot;<br>    - hidden: true<br>    - type: &quot;test&quot;<br>    - hidden: true<br>    presetConfig: true<br>  - path: &quot;@semantic-release/exec&quot;<br>    cmd: &quot;echo ${nextRelease.version} &gt; nextRelease&quot;<br>prepare:<br>  - path: &quot;@semantic-release/changelog&quot;<br>    changelogFile: &quot;docs/CHANGELOG.md&quot;<br>publish:<br>  - path: &quot;@semantic-release/github&quot;<br><br>success:<br>  - &quot;@semantic-release/github&quot;<br><br>fail:<br>  - &quot;@semantic-release/github&quot;</pre><p>Once these files have been committed and pushed to the branch specified in the above files ( prod in this case), either directly to the branch, or merge, GitHub Actions will trigger a run and create a release.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*v5oSMdc6sVtOJC1Qv6Hhmg.png" /><figcaption>Successful run of GitHub Action</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*j2BNXLpnD8EKC3PaVLX8Yg.png" /><figcaption>GitHub Action logs for a successful release</figcaption></figure><p>Releases for a GitHub repo can be viewed at /releases path on the URL for the repository.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fwm-BUP8yBtHY6IGm_V9rw.png" /><figcaption>GitHub Release and Tag</figcaption></figure><p>I hope you found this useful!<br>Cheers 🤙,</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4a9f28d965c" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Ingest EKS Fargate metrics to AWS Container Insights using ADOT with Helm]]></title>
            <link>https://cj-hewett.medium.com/ingest-eks-fargate-metrics-to-aws-container-insights-using-adot-with-helm-6edb452c82b0?source=rss-1b88832fa9b8------2</link>
            <guid isPermaLink="false">https://medium.com/p/6edb452c82b0</guid>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[helm]]></category>
            <category><![CDATA[fargate]]></category>
            <category><![CDATA[aws-eks-fargate]]></category>
            <category><![CDATA[opentelemetry]]></category>
            <dc:creator><![CDATA[CJ Hewett]]></dc:creator>
            <pubDate>Wed, 14 Jun 2023 07:37:47 GMT</pubDate>
            <atom:updated>2023-06-14T07:37:47.461Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/0*h06xrwCHQdOg5A52.png" /></figure><p>The following link shows how to deploy the ADOT Collector to EKS Fargate that enables the collection of Container Insights metrics from EKS Fargate workloads and send them to CloudWatch.</p><p>This is deployed via a couple YAML files that require some editing to work for your cluster. This article will go through deploying the same ADOT collector via a Helm chart, as well some of the prerequisites required for ADOT to run.</p><p><a href="https://aws-otel.github.io/docs/getting-started/container-insights/eks-fargate">Container Insights EKS Fargate | AWS Distro for OpenTelemetry</a></p><h3>Prerequisites</h3><p>It is assumed you have an EKS Fargate Cluster running.</p><p>The following resources are created with Terraform as an example, but they could be done through CLI or another method.</p><h4>Cert Manager</h4><p>You will need to install <a href="https://cert-manager.io/docs/installation/helm/">Cert Manager</a> for ADOT to work. <a href="https://docs.aws.amazon.com/eks/latest/userguide/adot-reqts.html">More info on why this is required</a>.</p><p>Change the webhook port for it to work on Fargate, as the Kubelet listens on port 10250 by default and clashes with Cert Manager.</p><pre>resource &quot;helm_release&quot; &quot;cert_manager&quot; {<br>  depends_on = [<br>    aws_eks_addon.coredns<br>  ]<br><br>  name             = &quot;cert-manager&quot;<br>  chart            = &quot;cert-manager&quot;<br>  create_namespace = true<br>  repository       = &quot;https://charts.jetstack.io&quot;<br>  version          = &quot;1.11.0&quot;<br>  namespace        = &quot;cert-manager&quot;<br><br>  set {<br>    name  = &quot;startupapicheck.timeout&quot;<br>    value = &quot;5m&quot;<br>  }<br>  set {<br>    name  = &quot;installCRDs&quot;<br>    value = &quot;true&quot;<br>  }<br>  set {<br>    name  = &quot;webhook.securePort&quot;<br>    value = &quot;10260&quot;<br>  }<br>}</pre><h4>AWS IAM Role for ADOT Service Account</h4><p>You’ll also need to set up an AWS IAM role for the ADOT Service Account to use with the following:</p><ul><li>a trust policy that provides federated access to the AWS EKS cluster through OIDC (I have used the “<a href="https://github.com/terraform-aws-modules/terraform-aws-eks">terraform-aws-modules/eks/aws</a>” Terraform module and am able to get the OIDC provider arn via module.eks.oidc_provider_arn)</li><li>The managed policy CloudWatchAgentServerPolicy so that ADOT has permissions to send Container Insights to CloudWatch.</li></ul><pre>variable &quot;service_account_namespace&quot; {<br>  default = &quot;fargate-container-insights&quot;<br>}<br><br>variable &quot;service_account_name&quot; {<br>  default = &quot;adot-collector&quot;<br>}<br><br>data &quot;aws_iam_policy_document&quot; &quot;adot_trust_policy&quot; {<br>  statement {<br>    actions = [&quot;sts:AssumeRoleWithWebIdentity&quot;]<br>    principals {<br>      type        = &quot;Federated&quot;<br>      identifiers = [module.eks.oidc_provider_arn]<br>    }<br>    condition {<br>      variable = &quot;${module.eks.oidc_provider}:sub&quot;<br>      test     = &quot;StringEquals&quot;<br>      values   = [&quot;system:serviceaccount:${var.service_account_namespace}:${var.service_account_name}&quot;]<br>    }<br>  }<br>}<br><br>resource &quot;aws_iam_role&quot; &quot;adot_service_acc&quot; {<br><br>  name = &quot;${var.env_name}-adot-role&quot;<br><br>  assume_role_policy = data.aws_iam_policy_document.adot_trust_policy.json<br><br>  managed_policy_arns = [<br>    &quot;arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy&quot;,<br>  ]<br>}<br><br>output &quot;adot_iam_role_arn&quot; {<br>  value = aws_iam_role.adot_service_acc.arn<br>}<br><br>resource &quot;kubernetes_namespace&quot; &quot;fargate_container_insights&quot; {<br>  metadata {<br>    name = var.service_account_namespace<br>  }<br>}</pre><p>The output adot_iam_role_arn will output the ARN of the IAM role which will be used in the ADOT Helm chart.</p><h3>Installation of the ADOT Fargate Helm chart</h3><p>Add the AWS Distro for OpenTelemetry Helm repository to Helm:</p><pre>helm repo add adot-fargate https://github.com/cjh-cloud/adot-fargate  <br>helm repo update</pre><p>Install the AWS Distro for OpenTelemetry Helm chart:<br>Change the cluster name, region to your own EKS cluster and IAM role ARN to the value of the adot_iam_role_arn Terraform output above.</p><pre>helm install adot-fargate cjh-cloud/adot-fargate \<br>  --set clusterName=my-cluster \<br>  --set region=ap-southeast-2 \<br>  --set serviceAccount.iamRoleArn=arn:aws:iam::[ACCOUNT ID]:role/adot-role</pre><p><em>OR</em> Install via repo:</p><pre>helm upgrade --install aws-distro-open-telemetry ./ \<br>  --set clusterName=my-cluster \<br>  --set region=ap-southeast-2 \<br>  --set serviceAccount.iamRoleArn=arn:aws:iam::[ACCOUNT ID]:role/adot-role</pre><p>Verify that the AWS Distro for OpenTelemetry deployment is running:</p><pre>kubectl get pods -n fargate-container-insights</pre><p>The output should show a pod with the name adot-collector-0.<br>View &quot;Container Insights&quot; under AWS CloudWatch to view EKS Cluster metrics.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rp9-XhPtWAtpZV5IrvesRA.png" /><figcaption>Performance Monitoring</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8ZBadWYNq4_u9qU4IkdfUw.png" /><figcaption>Container Map</figcaption></figure><p>Thanks for reading, I hope you found this useful!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6edb452c82b0" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building Docker images in GitHub Actions to be used by other workflows]]></title>
            <link>https://cj-hewett.medium.com/building-docker-images-in-github-actions-to-be-used-by-other-workflows-7bb75c35ecb0?source=rss-1b88832fa9b8------2</link>
            <guid isPermaLink="false">https://medium.com/p/7bb75c35ecb0</guid>
            <category><![CDATA[github-actions]]></category>
            <category><![CDATA[continuous-integration]]></category>
            <category><![CDATA[github]]></category>
            <category><![CDATA[ci]]></category>
            <category><![CDATA[docker]]></category>
            <dc:creator><![CDATA[CJ Hewett]]></dc:creator>
            <pubDate>Tue, 21 Feb 2023 01:02:43 GMT</pubDate>
            <atom:updated>2023-02-21T01:02:43.614Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/875/0*AsvzMT7QrIw_Z1dk.png" /></figure><p>In this post, I’ll go over how to use a GitHub Action to build and push a runner image to GitHub Packages, and then use said runner image as the base for another action. These two actions will be part of the same GitHub repository.</p><p>All code referenced is also in this repository can be found here:</p><p><a href="https://github.com/cjh-cloud/collective/tree/terraform">GitHub - cjh-cloud/collective at terraform</a></p><p>In this example, I have some Terraform in the branch that I want to be able to run plans on within a GitHub action. The GitHub ubuntu-latest runner does not have Terraform installed, which means we could either install it in the pipeline. However, it could be more efficient to use an image that has Terraform, and any other tools we need, pre-installed.</p><p>We could define this image with a Dockerfile in our repo, and have an action that builds this image and pushes it to GitHub packages only when there are changes to that Dockerfile. (As we don’t need to build a new image every time there are changes to the whole repository).</p><h4>tooling.yaml — Workflow to create the tooling image</h4><p>This is a pretty simple action that will only run on commits that have changed to the Dockerfile in the tooling directory. It will build a Docker image and push it to GitHub packages in the same repo. The image will have the name of the repo, and be tagged with the branch name. The name of this repo is collective and all this code is in the terraform branch, so the image name with be ghcr.io/cjh-cloud/collective:terraform.</p><p>We don’t need to set up a secret for GITHUB_TOKEN (line 28) in the repository as GitHub automates the authorisation for us during the workflow run.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/95c199d66ff9ff3fefad09c67b50ae06/href">https://medium.com/media/95c199d66ff9ff3fefad09c67b50ae06/href</a></iframe><p>We can see within GitHub that we now have a package 📦 as part of the repo:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kUYmR7XDAw8UXaoPs3A3Pg.png" /><figcaption>The Docker image in GitHub packages.</figcaption></figure><h4>infra.yaml — Workflow to use the tooling image to run Terraform</h4><p>The terraform job uses the container parameter to set the Docker image to run the job on, in this case the image we created with the previous Action. We can then checkout the repo and run Terraform commands in the pipeline, since the image that was built has Terraform installed.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/034a95f3f1d2a410087d33667cf750c8/href">https://medium.com/media/034a95f3f1d2a410087d33667cf750c8/href</a></iframe><p>It’s possible to create any kind of image with the tools you need installed, thereby saving time for the workflow that uses this image by eliminating the need to install these tools each time it runs.</p><p>If this helped you in any way, please consider giving me a follow. I’m trying to get to 100 followers, so it would mean a lot!</p><p>Cheers 🤙</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7bb75c35ecb0" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Configure Docker to use a proxy server in Jenkins]]></title>
            <link>https://cj-hewett.medium.com/configure-docker-to-use-a-proxy-server-in-jenkins-d4ec014d7fde?source=rss-1b88832fa9b8------2</link>
            <guid isPermaLink="false">https://medium.com/p/d4ec014d7fde</guid>
            <category><![CDATA[jenkins]]></category>
            <category><![CDATA[jenkinsfile]]></category>
            <category><![CDATA[jenkins-pipeline]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[docker]]></category>
            <dc:creator><![CDATA[CJ Hewett]]></dc:creator>
            <pubDate>Sat, 21 Jan 2023 02:35:45 GMT</pubDate>
            <atom:updated>2023-01-21T02:35:45.083Z</atom:updated>
            <content:encoded><![CDATA[<p>…by using Docker in Docker.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*RZBKHBWPqusveBPd.png" /></figure><p>I found that I couldn’t configure Jenkins to use a proxy server for Docker. However, I could spin up a Docker container within the pipeline that could. This is how to do that…</p><p>Have a docker in docker image with the AWS cli installed that can be puled by Jenkins. In the example below I’ve used “docker_tooling:latest” to represent this.</p><p>This Jenkinsfile consists of a stage that will spin up a “dind” container with a configured proxy, which can then be used in later steps by running docker exec commands against it.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/c0672956212825344eef1405f67e0bb8/href">https://medium.com/media/c0672956212825344eef1405f67e0bb8/href</a></iframe><p>This isn’t a fantastic solution, but helps if you’re in a pickle.</p><p>I hope you found this useful!<br>Cheers 🤙</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d4ec014d7fde" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Using Templates in Terraform to generate Kubernetes YAML]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://cj-hewett.medium.com/using-templates-in-terraform-to-generate-kubernetes-yaml-5f60cfa0109?source=rss-1b88832fa9b8------2"><img src="https://cdn-images-1.medium.com/max/1200/0*zgA9tq1VmoG6VQdm.png" width="1200"></a></p><p class="medium-feed-snippet">The tricky thing with YAML files in Kubernetes, is that it&#x2019;s not so easy to use variables within the files to inject values into them&#x2026;</p><p class="medium-feed-link"><a href="https://cj-hewett.medium.com/using-templates-in-terraform-to-generate-kubernetes-yaml-5f60cfa0109?source=rss-1b88832fa9b8------2">Continue reading on Medium »</a></p></div>]]></description>
            <link>https://cj-hewett.medium.com/using-templates-in-terraform-to-generate-kubernetes-yaml-5f60cfa0109?source=rss-1b88832fa9b8------2</link>
            <guid isPermaLink="false">https://medium.com/p/5f60cfa0109</guid>
            <category><![CDATA[terraform]]></category>
            <category><![CDATA[yaml]]></category>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[template]]></category>
            <category><![CDATA[yaml-generation]]></category>
            <dc:creator><![CDATA[CJ Hewett]]></dc:creator>
            <pubDate>Fri, 09 Dec 2022 20:07:44 GMT</pubDate>
            <atom:updated>2023-06-04T23:28:31.663Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Getting Docker in Docker with Rootless to run]]></title>
            <link>https://cj-hewett.medium.com/getting-docker-in-docker-with-rootless-to-run-5f91c7f6bc6?source=rss-1b88832fa9b8------2</link>
            <guid isPermaLink="false">https://medium.com/p/5f91c7f6bc6</guid>
            <category><![CDATA[rootless]]></category>
            <category><![CDATA[dind]]></category>
            <category><![CDATA[docker]]></category>
            <category><![CDATA[docker-in-docker]]></category>
            <dc:creator><![CDATA[CJ Hewett]]></dc:creator>
            <pubDate>Mon, 26 Sep 2022 05:57:43 GMT</pubDate>
            <atom:updated>2022-09-26T05:57:43.289Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*uTt9G36w8WtivcGO.png" /></figure><p>I’ve found that when trying to run the docker-in-docker image with rootless, I get the following error:</p><pre>Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?</pre><p>Turns out this is because the path to the Docker Host is actually unix:///run/user/1000/docker.sock and this was unfortunately left out of the documentation in the Docker docs (at the time of writing).</p><p>To get around this, you can use the following Dockerfile</p><pre>FROM docker:20.10.10-dind-rootless</pre><pre>ENV DOCKER_HOST=unix:///run/user/1000/docker.sock</pre><p>and then this docker-compose.yml file:</p><pre>version: &#39;3.8&#39;</pre><pre>services:<br>  dindtest:<br>    build:<br>      context: .<br>    privileged: true</pre><p>and then run docker-compose up and open another terminal and run docker exec -it [CONTAINER NAME] sh and you should be able to start running docker commands from within the container.</p><p>I hope you found this useful.<br>Chur 🤙,</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5f91c7f6bc6" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>