<?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 Katoria Henry on Medium]]></title>
        <description><![CDATA[Stories by Katoria Henry on Medium]]></description>
        <link>https://medium.com/@Katoria1?source=rss-6e1dbb973ea1------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*Gb7mAIYee2VMZl932GqAng.jpeg</url>
            <title>Stories by Katoria Henry on Medium</title>
            <link>https://medium.com/@Katoria1?source=rss-6e1dbb973ea1------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 08:33:02 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@Katoria1/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[Everything Was Green. Nothing Was Deploying.]]></title>
            <link>https://levelup.gitconnected.com/everything-was-green-nothing-was-deploying-bdfbce6dec1c?source=rss-6e1dbb973ea1------2</link>
            <guid isPermaLink="false">https://medium.com/p/bdfbce6dec1c</guid>
            <category><![CDATA[kargo]]></category>
            <category><![CDATA[devsecops]]></category>
            <category><![CDATA[terraform]]></category>
            <category><![CDATA[argo-cd]]></category>
            <category><![CDATA[aws-eks]]></category>
            <dc:creator><![CDATA[Katoria Henry]]></dc:creator>
            <pubDate>Wed, 09 Sep 2026 16:10:04 GMT</pubDate>
            <atom:updated>2026-09-09T16:10:04.005Z</atom:updated>
            <content:encoded><![CDATA[<h4>What three days of building a DevSecOps platform taught me about the failures you can’t search for</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3TREolt8hwWzwU8P6Ib6OA.png" /><figcaption>The dashed lines are the point. Kargo polls the registry and opens merge requests against git; ArgoCD reconciles what gets merged. This is a promotion system with a human in it, not a deploy script.</figcaption></figure><p>The promotion reported seven steps, all green. Kargo cloned the repository, rewrote the image tag, committed, pushed a branch, opened a merge request, waited for me to approve it, and told ArgoCD to sync. ArgoCD said Synced. ArgoCD said Healthy.</p><p>The pod was running the previous image.</p><p>Nothing had failed. No error appeared anywhere in the chain. Every dashboard I had built specifically to catch this said the system was working, and the system was not working.</p><p>That failure, and eight or nine others like it, is what I actually want to write about. The architecture is the least interesting part of this project. You can read a hundred posts describing an EKS platform with ArgoCD and GitOps. What you can’t easily find is a list of the things that break in ways that produce no error, and how you notice.</p><h3>What I built, briefly 🔥</h3><p>Over three days I built a <a href="https://gitlab.com/devsecops-platform-public">four-tier EKS platform</a> with an enforced software supply chain. The short version:</p><ul><li><strong>Terraform and Terragrunt</strong> for infrastructure, modular, with tiers as thin overrides. Adding a tier is four files averaging twenty lines.</li><li><strong>ArgoCD</strong> in an app-of-apps pattern. Terraform creates exactly one Kubernetes object; the root Application and everything after that is reconciled from git.</li><li><strong>GitLab CI</strong> building a hardened multi-stage image: FIPS-mode Go on a distroless static base, 7.5 MB, no shell, no package manager, no libc.</li><li><strong>Trivy and Kubescape</strong> as blocking gates, <strong>syft</strong> for SBOMs, <strong>cosign</strong> signing with an AWS KMS key.</li><li><strong>Kyverno</strong> verifying those signatures at admission, in Enforce mode, and rewriting image tags to the verified digest.</li><li><strong>Kargo</strong> promoting images between tiers by opening merge requests.</li><li><strong>Velero</strong> for backups, <strong>Coroot</strong> for eBPF observability, <strong>Pod Security Admission</strong> and <strong>NetworkPolicy</strong> for workload isolation.</li></ul><p>No long-lived cloud credentials anywhere. IRSA for everything in-cluster, GitLab OIDC for CI. The only stored secrets are GitLab tokens in AWS Secrets Manager, pulled at runtime by External Secrets.</p><p>All of it built and broken in a development cluster costing about seven dollars a day, destroyed and rebuilt daily.</p><p>That last point turns out to be the whole argument, and I’ll come back to it.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XdKKN0H5aZpxyl7BQpg3PA.png" /><figcaption>72 hours of pure crying and fun 😄</figcaption></figure><h3>Failure one: the promotion that changed nothing</h3><p>Back to the green promotion.</p><p>The cause was two images: entries in a Kustomize overlay. My base Deployment used a short alias:</p><pre>image: my-app</pre><p>with the overlay mapping it to the real registry path:</p><pre>images:<br>  - name: my-app<br>    newName: &lt;account&gt;.dkr.ecr-fips.us-east-1.amazonaws.com/dsp-shared/my-app<br>    newTag: 7fffedb8</pre><p>Kargo’s kustomize-set-image step doesn&#39;t know about that indirection. It writes an entry keyed on the <strong>full registry path</strong>:</p><pre>images:<br>  - name: &lt;account&gt;.dkr.ecr-fips.us-east-1.amazonaws.com/dsp-shared/my-app<br>    newTag: 7481984b        # Kargo&#39;s edit<br>  - name: my-app<br>    newName: &lt;account&gt;.dkr.ecr-fips.us-east-1.amazonaws.com/dsp-shared/my-app<br>    newTag: 7fffedb8        # the original, which wins</pre><p>Two entries, both valid, both matching nothing the other matched. Kustomize applied the second. Kargo had done exactly what it was asked. ArgoCD had synced exactly what git contained. The deployed image never changed.</p><p>The fix was making the base reference the full path so both tools key on the same string. The lesson was the diagnostic:</p><pre>kubectl kustomize apps/my-app/overlays/dev | grep &quot;image:&quot;</pre><p>One line, one tag. Run it before merging a promotion. And after the promotion completes, check the pod rather than the promotion:</p><pre>kubectl -n my-app get pod -o jsonpath=&#39;{.items[0].spec.containers[0].image}&#39;</pre><p>Green statuses are a claim about process. The running image is a fact.</p><h3>Failure two: ArgoCD reported Synced while refusing to deploy</h3><p>This one I caused myself, trying to fix something cosmetic 😢.</p><p>Kyverno’s mutateDigest rewrites an image reference at admission so what runs is provably the bytes that passed signature verification:</p><pre>my-app:7481984b@sha256:095729a0c4e28cf2c331645514718628a91ef06...</pre><p>Git has only the tag. ArgoCD compares live to desired, sees a difference it can never reconcile, and reports OutOfSync forever. Worse, selfHeal keeps trying to strip the digest that Kyverno keeps re-adding, resulting in two controllers quietly fighting over one field.</p><p>The obvious fix is to tell ArgoCD to ignore that field:</p><pre>ignoreDifferences:<br>  - group: apps<br>    kind: Deployment<br>    jqPathExpressions:<br>      - &#39;.spec.template.spec.containers[].image&#39;</pre><p>That works and then boom, the Application goes green.</p><p>It also means <strong>ArgoCD can no longer see legitimate image changes</strong>. When Kargo updated the tag in git, ArgoCD compared the live Deployment to the desired one, found no difference in any field it was allowed to look at, and reported Synced. It deployed nothing. There was no error, no warning, no OutOfSync marker. The dashboard was green and the system had silently stopped working.</p><p>I found it by forcing a sync manually and watching the image change.</p><p>The resolution was to accept a permanently OutOfSync-but-Healthy Application. A misleading status indicator is annoying. An Application that reports success while refusing to deploy is an outage waiting for someone to notice.</p><p>There’s a general principle here that cost me several hours across the project: <strong>anything that writes to a field git doesn’t control produces unreconcilable drift.</strong> I hit it four separate times before I recognized the pattern.</p><p>Each time, something other than git was writing to a field, so ArgoCD compared live state to desired state and found a difference it could never reconcile:</p><ul><li><strong>containers[].image</strong> — Kyverno&#39;s mutateDigest appends the verified digest. Git only has the tag, because the digest isn&#39;t known until verification happens.</li><li><strong>APIService.spec.caBundle</strong> — cert-manager injects a CA bundle generated in-cluster. That certificate cannot exist in git; it doesn&#39;t exist until the cluster creates it.</li><li><strong>Warehouse.spec.subscriptions</strong> — Kargo&#39;s admission webhook writes defaulted fields back onto its own custom resource.</li><li><strong>directory.recurse: false</strong> — the API server itself, and this one is its own category.</li></ul><p>That last one is worth its own sentence. I wrote recurse: false explicitly in an ArgoCD Application. false is the default, so the API server drops the key entirely. Git said false, the live object said nothing, and ArgoCD reported drift that could never be resolved.</p><p><strong>Never declare a field at its default value in a GitOps manifest.</strong></p><h3>Failure three: NetworkPolicy that isn’t enforced</h3><p>I wrote default-deny NetworkPolicies for every namespace (I know, many wouldn’t do this for shifting things left, but I DO 😀), plus explicit allows for DNS, same-namespace traffic, and the Kubernetes API. Applied them. kubectl get networkpolicy -A listed them all. Kubescape moved the relevant control from 0% to 68%.</p><p><strong>None of it was enforced </strong>😄😭<strong>.</strong></p><p>The AWS VPC CNI <strong>accepts NetworkPolicy objects and ignores them</strong> unless you explicitly turn on enforcement:</p><pre>configuration_values = jsonencode({ enableNetworkPolicy = &quot;true&quot; })</pre><p>This is worse than having no policies. With no policies you know you have no policies. With unenforced policies you have documentation that claims otherwise, a compliance scanner that agrees with the documentation, and no actual isolation.</p><p>The check takes one command:</p><pre>kubectl get ds aws-node -n kube-system \<br>  -o jsonpath=&#39;{.spec.template.spec.containers[*].name}&#39;<br># aws-node aws-eks-nodeagent</pre><p>No aws-eks-nodeagent, no enforcement.</p><p>Then, once enforcement was on, the policies broke ArgoCD.</p><p>Not immediately, and not in a way that looked like networking. Every ArgoCD Application went to <strong>Unknown; </strong>not OutOfSync, not Degraded. The application-controller logs showed connection timeouts to two addresses:</p><pre>dial tcp 172.20.222.51:8081: i/o timeout    # repo-server<br>dial tcp 172.20.180.188:6379: i/o timeout   # redis</pre><p>My VPC CIDR is 10.10.0.0/16. I had allowed egress to the VPC and to 0.0.0.0/0 on 443. But ClusterIP Service addresses come from the <strong>service CIDR</strong>, 172.20.0.0/16 on EKS, which is an entirely separate range. A pod calling a Service in its own namespace still egresses to 172.20.x.x.</p><p>Two rules were missing:</p><pre>egress:<br>  - to: [podSelector: {}]                       # same namespace, any port<br>  - to: [ipBlock: {cidr: 172.20.0.0/16}]        # ClusterIP Services</pre><p>The same class of mistake took down health probes later. Kubelet probes originate from the <strong>node</strong>, not from a pod, so a policy allowing only same-namespace ingress drops them. Pods sat at 0/1 Ready indefinitely with nothing in the container logs, because the request never arrived.</p><p>Blocked connections look like timeouts, not refusals. They present as slow startup, mysterious readiness failures, and controllers that report Unknown. They rarely look like a firewall.</p><h3>Failure four: thirty minutes of nothing (lol LITERALLY)</h3><p>I added a launch template to the node group which was needed for Bottlerocket’s SSM control container, and a good opportunity to set the IMDS hop limit to 1 so pods can’t reach node credentials.</p><p>The apply ran for twenty-eight minutes. I checked:</p><pre>aws eks describe-nodegroup --cluster-name dsp-dev-eks \<br>  --nodegroup-name dsp-dev-eks-ng --query &#39;nodegroup.{status:status,health:health}&#39;</pre><pre>{ &quot;status&quot;: &quot;CREATING&quot;, &quot;health&quot;: { &quot;issues&quot;: [] } }</pre><p>Creating, no issues. Meanwhile:</p><pre>{ &quot;desired&quot;: 3, &quot;instances&quot;: 0 }</pre><p>Zero instances. Every one was being terminated at launch, and the node group API reported nothing at all.</p><p>The reason was only visible in one place:</p><pre>aws autoscaling describe-scaling-activities --auto-scaling-group-name &quot;$ASG&quot; \<br>  --query &#39;Activities[].{status:StatusCode,cause:StatusMessage}&#39;</pre><pre>Client.InvalidKMSKey.InvalidState: The KMS key provided is in an incorrect state</pre><p>I had encrypted the root volumes with the cluster’s KMS key, the one used for envelope-encrypting Kubernetes Secrets. Encrypting EBS volumes with a customer managed key requires the Auto Scaling service-linked role to hold kms:CreateGrant on it. It doesn&#39;t, by default, and nothing tells you.</p><p>The fix was to use the AWS-managed EBS key, which encrypts at rest just as effectively. The cluster key’s job is etcd, not block storage.</p><p><strong>describe-nodegroup will not tell you why a node group is stuck.</strong> describe-scaling-activities will. That single command would have saved twenty-five minutes.</p><h3>Failure five: a chart pinned to an image that no longer exists</h3><p>I pinned every Helm chart to an exact version, which is correct practice and which I felt good about.</p><p>Then Kyverno’s cleanup jobs started failing with ImagePullBackOff, and an ArgoCD sync operation wedged waiting on a hook that could never complete. It blocked that Application four separate times over two days.</p><p>The chart was pinned. The chart hadn’t changed. The <strong>image the chart references</strong> had been retired from public Docker Hub by its vendor.</p><p>A pinned chart guarantees the chart doesn’t change. It says nothing about the images inside it.</p><p>The second instance of the same class was subtler. Another job referenced:</p><pre>image: busybox:1.35</pre><p>No registry prefix, so it resolves to Docker Hub, and anonymous pulls from AWS IP ranges hit Docker Hub’s rate limit. Intermittent ImagePullBackOff on a chart that worked yesterday and will work again tomorrow.</p><p>The answer to both is mirroring third-party images into a registry you control. Which introduced its own trap:</p><p><strong>Don’t mirror with </strong><strong>docker pull on an Apple Silicon machine.</strong> You&#39;ll pull arm64 images and push them into a registry serving amd64 nodes, and the failure appears at runtime as an exec format error. crane copy transfers the full multi-arch manifest registry-to-registry without pulling anything locally.</p><pre>crane manifest &lt;image&gt; | jq -r &#39;.manifests[].platform | &quot;\(.os)/\(.architecture)&quot;&#39;</pre><h3>Failure six: six (6) wrong Helm values paths</h3><p>This one is embarrassing in aggregate 😭.</p><p>Helm <strong>silently ignores unknown values keys.</strong> A wrong path produces no warning, no error, and no effect. The deployment succeeds. The setting does nothing.</p><p>I got six of them wrong across the project:</p><ul><li>Kyverno’s IRSA annotation lives at admissionController.rbac.serviceAccount.annotations, not admissionController.serviceAccount.annotations. The leaf key existed; I had the parent wrong.</li><li>Kubescape’s nodeAgent.enabled doesn&#39;t exist at all. The eBPF agent is controlled through capability flags, most of which default to enabled. My attempt to disable it did nothing, and would have deployed a privileged DaemonSet onto two t3.medium nodes.</li><li>Coroot’s communityEdition sounds like a boolean. It&#39;s an object; its <em>presence</em> enables the feature. communityEdition: true is rejected; communityEdition: {} is correct.</li><li>Coroot’s logCollector has no enabled field. It takes collectLogEntries and collectLogBasedMetrics.</li></ul><p>Each was found by reading the actual values file or CRD schema:</p><pre>helm show values &lt;repo&gt;/&lt;chart&gt; --version &lt;v&gt; &gt; /tmp/values.yaml<br>kubectl explain &lt;kind&gt;.spec.&lt;field&gt;<br>kubectl apply --dry-run=server -f &lt;file&gt;</pre><p>That last one is the best of the three. Server-side validation, one second, exact field name. Once I started running it before every commit, this entire class of failure disappeared.</p><p>There’s a variant when a CRD uses x-kubernetes-preserve-unknown-fields and there&#39;s no schema to read. Then you ask the webhook by submitting something deliberately wrong:</p><pre>spec.subscriptions[0].image.imageSelectionStrategy: Unsupported value:<br>&quot;DEFINITELY_NOT_VALID&quot;: supported values: &quot;Digest&quot;, &quot;Lexical&quot;,<br>&quot;NewestBuild&quot;, &quot;SemVer&quot;</pre><p>The rejection enumerates the valid values. Faster than searching documentation, and correct for the version you’re actually running.</p><h3>The failures that were the system working</h3><p>Not everything that looked like a problem was one.</p><p><strong>Pod Security Admission rejected my test pod</strong> with five specific violations — privileged, no allowPrivilegeEscalation: false, capabilities not dropped, not runAsNonRoot, no seccomp profile. That&#39;s the API server refusing a non-compliant workload before Kyverno was even consulted.</p><p><strong>A LimitRange rejected a pod requesting 16Mi</strong> because the namespace sets a 32Mi floor. The platform’s own guardrail catching the person who wrote it.</p><p><strong>Kyverno blocked an unsigned image</strong> pushed to the same repository as a signed one:</p><pre>resource Pod/my-app/blocked was blocked due to the following policies<br>verify-image-signatures:<br>  verify-signed-by-platform-key: &#39;failed to verify image ...: no signatures found&#39;</pre><p>That last test mattered more than the positive one. A policy that passes everything is indistinguishable from a policy that isn’t running. I only knew signature verification worked because I watched it refuse something.</p><p><strong>Nothing could be created while Kyverno was down.</strong> During a rolling node replacement its admission webhook lost all endpoints, and because the chart uses failurePolicy: Fail, every pod creation cluster-wide was blocked until it recovered.</p><p>That’s correct behavior, and it’s the strongest argument I have for three admission controller replicas in production before tightening failurePolicy on my own policies. Enforcement that can be bypassed by an outage is weaker than it looks. Enforcement that causes the outage is worse.</p><h3>What actually got built</h3><p>Setting the failures aside, the finished path:</p><p>A commit triggers a build of a 7.5 MB distroless image running FIPS-mode Go. <strong>Trivy </strong>scans it and fails on HIGH or CRITICAL findings that have a fix available. <strong>Syft </strong>generates SBOMs in two formats. The image is pushed to ECR with an immutable tag, then signed with an AWS KMS key, signing after the push, because a signature binds to a digest and the digest doesn’t exist until the push completes.</p><p><a href="https://kargo.io/"><strong>Kargo</strong></a> polls ECR, notices the new image, and creates Freight. Promoting that Freight clones the GitOps repository, rewrites the image tag in the target overlay, and <strong>opens a merge request</strong>. It holds a token that could push directly to a protected branch. It deliberately doesn’t. The promotion blocks until a human merges.</p><p><a href="https://argoproj.github.io/cd/"><strong>ArgoCD </strong></a>syncs. At admission, Pod Security Admission enforces restricted, <a href="https://kyverno.io/"><strong>Kyverno</strong></a> verifies the cosign signature against a public key committed to git; no KMS call, no AWS dependency in the path that decides whether a pod may start — and rewrites the tag to the verified digest. NetworkPolicy denies everything not explicitly allowed. The pod cannot reach the instance metadata service, blocked both by policy and by an IMDS hop limit of 1.</p><p><a href="https://velero.io/"><strong>Velero</strong></a><strong> </strong>backs it all up to a versioned S3 bucket via IRSA, <strong>and I tested a restore rather than assuming one.</strong></p><p>Three FIPS layers: the Go cryptographic module in the application, Bottlerocket FIPS AMIs on staging and production nodes, and FIPS endpoints for AWS API calls. The application reports its own mode at runtime, because a Dockerfile comment claiming FIPS proves nothing:</p><pre>{ &quot;go&quot;: &quot;go1.26.8&quot;, &quot;fipsMode&quot;: &quot;fips140=on&quot; }</pre><p>Production was stood up and torn down deliberately, which converted four assumptions into facts; most importantly that EKS merges its bootstrap configuration into Bottlerocket’s TOML user data, which if wrong would have meant nodes that never join. It cost about a dollar.</p><h3>Kargo promotion, seven green steps</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Qo0Gdc3zPbVTsW3-2vjrkA.png" /><figcaption><em>Seven steps, all green, status Succeeded. The deployed image did not change.</em></figcaption></figure><h3>Kargo pipeline view with “Waiting for Approval”</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hpbQZe2GL-v0MZ9wCnIEgg.png" /><figcaption><em>Kargo holds a token that could push directly to a protected branch. It opens a merge request instead and blocks until a human merges it.</em></figcaption></figure><h3>Kargo MR Submission</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YLsFO1ABfL-ykI65FNg02A.png" /><figcaption>MR for promotion submitted via Kargo; all MRs require human approval</figcaption></figure><h3>GitLab pipeline, seven jobs across five stages</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*IhJ6MWyfuF2JBbWmMLEL0w.png" /><figcaption><em>Build, scan, SBOM, publish, sign. Nothing reaches the registry until every gate passes; nothing is signed until it is in the registry, because a signature binds to a digest.</em></figcaption></figure><h3>ArgoCD applications tiles</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_e1UggjLRyeU1Y79pRn-8A.png" /><figcaption>Fifteen Applications, all reconciled from git. Terraform creates exactly one of them; the rest are discovered from the root Application.</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/970/1*NLQ64vByrICZWQkZ9W-a_w.png" /><figcaption>Kargo-pipelines remained OutOfSync for hours, despite reporting as “healthy”</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/963/1*iR0Izrt4Qeios6IwTyxAdQ.png" /><figcaption>Kubescape, cert-manager, and platform-dev were the least problematic ArgoCD Apps</figcaption></figure><h3>Coroot service map</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UzQoFYE0VRwA4w-_pZKp6g.png" /><figcaption><em>Coroot builds this from eBPF alone. No instrumentation, no sidecars, no application changes. The SSM agent connection is the private-endpoint access path, discovered rather than declared.</em></figcaption></figure><h3>CloudWatch audit log group</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VC_HNyM7z29ccRDtXEprfA.png" /><figcaption><em>All five control plane log types enabled, including audit. Retention is three days in dev and 365 in production.</em></figcaption></figure><h3>The part I actually want you to take away</h3><p>Every failure in this post happened in a development cluster.</p><p>Signature verification was configured wrong four times before it worked. The wrong Kyverno values path. Credentials the admission controller couldn’t use. A transparency log setting that had to match on both the signing and verifying side. A mutateDigest incompatibility with Audit mode that Kyverno rejects outright.</p><p>Four failures, each diagnosed in minutes, against a cluster nobody depended on and that I destroyed at the end of the day.</p><p>The same four failures against production would have been an incident, a change freeze, a rollback, and a meeting.</p><p>“Shift security left” usually means scanning earlier in the pipeline. That’s fine, and it’s the easy part. The version I think is more useful: <strong>build the enforcement, break it deliberately, and learn its failure modes somewhere the cost of being wrong is a rebuild.</strong></p><p>You cannot learn that NetworkPolicy silently isn’t enforced on EKS by reading about NetworkPolicy. You learn it by writing policies, believing they work, and then testing whether a pod can actually reach something it shouldn’t.</p><p>You cannot learn that ignoring a field in ArgoCD can silently stop deployments by reading the ArgoCD documentation. You learn it by doing it and noticing your image didn’t change.</p><p>The cluster cost seven dollars a day. Every one of these lessons was available for less than the price of lunch, and every one of them would have been considerably more expensive to learn later.</p><p><em>The platform is Terraform, Terragrunt, EKS, ArgoCD, Kargo, Kyverno, Kubescape, Cosign, Velero, Coroot and Git</em></p><p>Follow me on LinkedIn 😄!</p><p>https://www.linkedin.com/in/katoria-henry-2018</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=bdfbce6dec1c" width="1" height="1" alt=""><hr><p><a href="https://levelup.gitconnected.com/everything-was-green-nothing-was-deploying-bdfbce6dec1c">Everything Was Green. Nothing Was Deploying.</a> was originally published in <a href="https://levelup.gitconnected.com/">Level Up Coding</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building a Containerized ML Inference Service with Automated CI/CD]]></title>
            <link>https://levelup.gitconnected.com/building-a-containerized-ml-inference-service-with-automated-ci-cd-1e987e8d5dfa?source=rss-6e1dbb973ea1------2</link>
            <guid isPermaLink="false">https://medium.com/p/1e987e8d5dfa</guid>
            <category><![CDATA[github-actions]]></category>
            <category><![CDATA[docker-model-runner]]></category>
            <category><![CDATA[automation]]></category>
            <category><![CDATA[ci-cd-pipeline]]></category>
            <category><![CDATA[machine-learning]]></category>
            <dc:creator><![CDATA[Katoria Henry]]></dc:creator>
            <pubDate>Tue, 13 Jan 2026 20:35:00 GMT</pubDate>
            <atom:updated>2026-01-18T10:51:13.992Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1017/1*pIHLrFglVLir9yAJZwok-Q.png" /></figure><p>I built a Docker-based machine learning inference service with automated security scanning, testing, and deployment. Every git push triggers a pipeline that scans for vulnerabilities, builds the image, tests it, and pushes to Docker Hub , which can be ready for a general development deployment or production deployment with additional pieces that I call out below. The workflow can also be triggered manually (workflow dispatch) if you’re making changes within the GitHub console directly.</p><p>Full <a href="https://github.com/theCaptN21/docker-model-runner">GitHub Repo </a>if you’d like to test this out on your own!</p><h3>What This Actually Is</h3><p>Let’s be clear: this is <strong>not</strong> Docker Desktop’s “Model Runner” CLI tool. This is a <strong>containerized inference service</strong> — a Flask API that loads a Hugging Face model into memory and serves predictions locally.</p><p><strong>The architecture:</strong></p><p>Container starts → Loads distilgpt2 into RAM → Exposes API → Serves inference</p><p><strong>What happens on each request:</strong></p><p>POST /generate → Tokenize → Run model (in container) → Return text</p><p>No external API calls. No proxying. The model runs in your container.</p><h3>Why This Matters</h3><p>Most ML tutorials show you how to train a model. Few show you how to <strong>actually deploy it</strong> with:</p><ul><li>Automated security scanning (Trivy + SonarCloud)</li><li>Multi-stage Docker builds with hardening</li><li>CI/CD pipeline that catches issues before production</li><li>Real-world testing and deployment</li></ul><p>This project demonstrates the <strong>deployment side</strong> of ML engineering.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vk_N2R18DxTeuf-EuOErTw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0tJWEBjp2rTnVk2A-2NeyA.png" /><figcaption>Github Actions workflow &amp; Report Summary</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5TLwngoPrKW8FY_2rp2s7w.png" /><figcaption>Trivy scan results</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*W-hs8mWMaMqm1iMMdiQjtA.png" /><figcaption>SonarQube results/recommendations</figcaption></figure><h3>Proving It’s Actually a Model Runner</h3><p><strong>Challenge:</strong> “This is just a Hugging Face API wrapper!”</p><p><strong>Proof it’s not:</strong></p><ol><li><strong>Disconnect from internet:</strong></li></ol><pre>docker run -d -p 5000:5000 model-runner:latest<br># Turn off WiFi<br>curl http://localhost:5000/generate -d &#39;{&quot;prompt&quot;:&quot;test&quot;}&#39;<br># Still works</pre><p><strong>2. Check memory usage:</strong></p><pre>docker stats<br># 600MB RAM used (model loaded in memory)</pre><p><strong>3. Examine the container:</strong></p><pre>docker exec model-runner ls /root/.cache/huggingface/<br># Model files present: pytorch_model.bin (~250MB)</pre><p>The model is <strong>downloaded during build</strong>, <strong>loaded into memory on startup</strong>, and <strong>inference runs locally</strong>. This is a real inference service.</p><h3>The CI/CD Pipeline</h3><p>Every push triggers:</p><ol><li><strong>SonarCloud</strong> — Code quality &amp; security analysis</li><li><strong>Trivy</strong> — Dependency vulnerability scanning</li><li><strong>Build</strong> — Multi-stage Docker image</li><li><strong>Test</strong> — Automated health checks and API tests</li><li><strong>Deploy</strong> — Push to Docker Hub</li></ol><p><strong>Real impact:</strong> Caught CVE-2024–11392 (CRITICAL) in transformers 4.35.0, updated to 4.48.0 before deployment.</p><h3>What It’s Missing (Honest Assessment)</h3><p>This is a <strong>starter template</strong>, not turnkey production. You’d still need:</p><ul><li>Authentication (API keys, OAuth)</li><li>Rate limiting</li><li>Monitoring &amp; observability</li><li>Horizontal scaling setup</li><li>Production-grade error handling</li></ul><p>But it gives you the <strong>foundation</strong> with security scanning and automated deployment already configured. As an engineer, having a solid starting point is valuable — but understanding how to build <strong>from</strong> it? That’s where the real learning happens.</p><h3>Quick Start</h3><p># Fork the repo on GitHub, then clone YOUR fork</p><pre>git clone https://github.com/YOUR_USERNAME/docker-model-runner.git<br>cd docker-model-runner<br># Test locally<br>make test<br># Configure GitHub secrets (5 required):<br># - SONAR_TOKEN, SONAR_PROJECT_KEY, SONAR_ORGANIZATION<br># - DOCKERHUB_USERNAME, DOCKERHUB_TOKEN<br># Push and watch automation<br>git push origin main</pre><p><strong>~20 minutes later:</strong> Your ML inference service (image) is live on Docker Hub.</p><h3>Architecture Deep Dive</h3><p><strong>What Hugging Face Transformers does:</strong></p><pre>generator = pipeline(&#39;text-generation&#39;, model=&#39;distilgpt2&#39;)</pre><p>This line:</p><ol><li>Downloads the model from Hugging Face Hub (~250MB)</li><li>Loads PyTorch weights into memory</li><li>Initializes tokenizer</li><li>Creates the inference pipeline</li><li>Caches everything</li></ol><p><strong>When you call inference:</strong></p><pre>result = generator(&quot;Hello&quot;, max_length=50)</pre><p>This:</p><ol><li>Tokenizes input (in the container)</li><li>Runs transformer layers (in the container)</li><li>Generates tokens auto-regressively (in the container)</li><li>Decodes output (in the container)</li><li>Returns text</li></ol><p><strong>Zero external dependencies after model download.</strong></p><h3>Performance</h3><p><strong>Cold start:</strong> 40–60 seconds (model loading)<br> <strong>Warm inference:</strong> 100–300ms per request<br> <strong>Memory:</strong> ~600MB per container<br> <strong>Scaling:</strong> Horizontal (multiple containers) or vertical (bigger instances)</p><h3>When to Use This</h3><p><strong>Use this approach when:</strong></p><ul><li>Building custom ML APIs with full control</li><li>You need integrated CI/CD for ML deployments</li><li>You want to learn ML deployment best practices</li><li>Building a portfolio project</li></ul><p><strong>Don’t use this when:</strong></p><ul><li>You’re just experimenting locally (use Docker Model CLI instead)</li><li>You want a fully managed service (use the Hugging Face API)</li><li>You need &lt;50ms latency (requires GPU optimization)</li></ul><h3>What You’ll Learn</h3><ol><li>How to containerize ML models properly</li><li>Multi-stage Docker builds for smaller, secure images</li><li>CI/CD pipelines for automated testing and deployment</li><li>Security scanning integration (Trivy, SonarCloud)</li><li>Real-world ML deployment patterns</li></ol><h3>The Key Insight</h3><p>The hard part of ML isn’t training models — it’s <strong>deploying them reliably</strong>. This project shows you:</p><ul><li>How to package models in containers</li><li>How to automate security scanning</li><li>How to test inference services</li><li>How to deploy with confidence</li></ul><p><strong>Everything else is in the repo.</strong> The README has a step-by-step setup, troubleshooting, customization options, and production considerations.</p><h3>Quick Links</h3><ul><li><strong>📁 GitHub Repository:</strong> [<a href="https://github.com/theCaptN21/docker-model-runner">docker-model-runner</a>] (Complete setup instructions)</li><li><strong>🔍 Local testing:</strong></li></ul><pre>#Pull the image &amp; run the container<br>docker pull 021bl00m/docker-model-runner:latest<br>docker run -d -p 5001:5000 --name model-runner 021bl00m/docker-model-runner:latest<br><br># Watch the logs<br>docker logs -f model-runner<br><br># Look for: &quot;Model loaded successfully&quot;<br># Press Ctrl+C to exit logs<br><br>#Test the API<br>curl http://localhost:5001/health<br><br>#Generate text<br>   curl -X POST http://localhost:5001/generate \<br>     -H &quot;Content-Type: application/json&quot; \<br>     -d &#39;{&quot;prompt&quot;: &quot;Hello AI&quot;, &quot;max_length&quot;: 50}&#39;<br><br># Creative writing<br>curl -X POST http://localhost:5001/generate \<br>  -H &quot;Content-Type: application/json&quot; \<br>  -d &#39;{&quot;prompt&quot;: &quot;Once upon a time&quot;, &quot;max_length&quot;: 100}&#39;<br><br># Technical content<br>curl -X POST http://localhost:5001/generate \<br>  -H &quot;Content-Type: application/json&quot; \<br>  -d &#39;{&quot;prompt&quot;: &quot;Machine learning is&quot;, &quot;max_length&quot;: 80}&#39;<br><br>#Clean up<br>docker stop model-runner<br>docker rm model-runner<br><br></pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9X8QYZ7CQp2tUASnDOpRfg.png" /><figcaption>Local Testing output</figcaption></figure><h3>Next Steps</h3><ol><li>Fork the repo to your GitHub account</li><li>Clone your fork and test locally with make test</li><li>Set up 5 GitHub secrets (instructions in the README)</li><li>Push changes and watch your automated pipeline</li><li>Customize for your use case</li></ol><p>This is a <strong>learning project</strong> that demonstrates ML deployment fundamentals. It’s a foundation, not a complete production solution. Use it to understand the patterns, then build on it.</p><p><strong>The repo has everything you need.</strong> Go build something!! 🚀</p><p>Following me on LinkedIn @https://www.linkedin.com/in/katoria-henry-2018</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1e987e8d5dfa" width="1" height="1" alt=""><hr><p><a href="https://levelup.gitconnected.com/building-a-containerized-ml-inference-service-with-automated-ci-cd-1e987e8d5dfa">Building a Containerized ML Inference Service with Automated CI/CD</a> was originally published in <a href="https://levelup.gitconnected.com/">Level Up Coding</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building AI-Powered Marvel Avengers Chatbots with AWS Bedrock and Claude Sonnet 4.5]]></title>
            <link>https://levelup.gitconnected.com/building-ai-powered-marvel-avengers-chatbots-with-aws-bedrock-and-claude-sonnet-4-5-8809ed4b722a?source=rss-6e1dbb973ea1------2</link>
            <guid isPermaLink="false">https://medium.com/p/8809ed4b722a</guid>
            <category><![CDATA[boto3]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[docker]]></category>
            <category><![CDATA[aws-bedrock]]></category>
            <category><![CDATA[aws]]></category>
            <dc:creator><![CDATA[Katoria Henry]]></dc:creator>
            <pubDate>Sun, 14 Dec 2025 19:56:00 GMT</pubDate>
            <atom:updated>2025-12-15T04:54:32.472Z</atom:updated>
            <content:encoded><![CDATA[<p>A sample guide to deploying character-based AI agents using Docker, CloudFormation, and AWS Bedrock.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/533/1*45MQOGrZftzOE8pIY5L82Q.png" /></figure><h3>Introduction</h3><p>Have you ever wanted to chat with Iron Man about your latest tech project? Or get strategic advice from Black Widow?</p><p>If you’re a Mac user, especially with Apple Silicon, you’ve probably hit the frustrating wall of “this AI tutorial doesn’t work on M1/M2/M3” or spent hours debugging architecture compatibility issues. Docker containers fail with cryptic errors. Python packages won’t install. AWS tools seem built for Linux developers only.</p><p><strong>I’ve been there. And I fixed it.</strong></p><p>In this tutorial, we’ll build AI agents that embody Marvel’s Avengers characters using AWS Bedrock and Claude Sonnet 4.5, <strong>specifically designed for Mac users</strong>. No more “this example is for x86_64 only” disclaimers. No more wrestling with Rosetta errors. No more feeling like a second-class developer.</p><p>By the end of this guide, you’ll have:</p><ul><li>A containerized development environment optimized for Apple Silicon</li><li>Infrastructure-as-Code using AWS CloudFormation</li><li>Fully deployed AI agents with custom Marvel personalities</li><li>Secure S3 storage with encryption and access controls</li></ul><p><strong>What makes this project special:</strong></p><ul><li>All code is reusable and <em>can be</em> production-ready, with a few security tweaks</li><li>Security best practices built-in from day one</li><li>One-command deployments for any character</li><li>Interactive chat interface included</li></ul><p>Let’s assemble!</p><h3>What We’re Building</h3><p>We’re creating a complete deployment system for AI agents powered by Claude Sonnet 4.5, AWS’s most advanced AI model. Each agent takes on the personality of a Marvel character, responding in-character with appropriate speech patterns and references.</p><p><strong>Tech Stack:</strong></p><ul><li><strong>AWS Bedrock: </strong>Managed AI service</li><li><strong>Claude Sonnet 4.5: </strong>Advanced language model</li><li><strong>CloudFormation: </strong>Infrastructure-as-Code</li><li><strong>Docker: </strong>Containerized development</li><li><strong>Python &amp; Boto3: </strong>Agent testing and interaction</li><li><strong>S3: </strong>Secure storage with encryption</li></ul><p><strong>Architecture Overview:</strong></p><pre>Local Machine (Docker)<br>        ↓<br>   AWS Bedrock Agent<br>        ↓<br>   Claude Sonnet 4.5<br>        ↓<br>   Personalized Response</pre><h3>Prerequisites</h3><p>Before we start, make sure you have:</p><ol><li><strong>macOS</strong> (M1/M2/M3 or Intel)</li><li><strong>Docker Desktop</strong> installed</li><li><strong>AWS Account</strong> with Bedrock access</li><li><strong>AWS CLI</strong> configured locally</li><li><strong>Basic terminal</strong> knowledge</li></ol><p><strong>Cost Estimate:</strong></p><ul><li>AWS Bedrock: Pay-per-use (~$0.003 per 1K tokens)</li><li>S3 Storage: ~$0.023 per GB/month</li><li><strong>Total for tutorial</strong>: Less than $1</li></ul><h3>Part 1: Project Setup</h3><h3>Clone the Repository</h3><p>First, let’s get the code:</p><pre>git clone https://github.com/theCaptN21/avengers-bedrock-agents.git<br>cd avengers-bedrock-agents</pre><p>Copy the environment template and add your credentials:</p><pre>cp .env.example .env<br>vi .env</pre><p>Add your AWS credentials :</p><pre># If using AWS IAM<br>AWS_ACCESS_KEY_ID=your_access_key<br>AWS_SECRET_ACCESS_KEY=your_secret_key<br>AWS_DEFAULT_REGION=us-east-1</pre><pre><br>AWS Profile (if using aws configure or aws login)<br>AWS_PROFILE=default</pre><blockquote><strong><em>Security Note:</em></strong><em> The </em><em>.env file is in </em><em>.gitignore and will never be committed to Git. Your credentials stay local.</em></blockquote><h3>Part 2: Understanding the Architecture</h3><p>Let’s break down what makes this system work.</p><h3>Multi-Stage Docker Container</h3><p>Our Dockerfile uses a multi-stage build for efficiency:</p><p><strong>Stage 1:</strong> Base system with Python 3.10</p><p><strong>Stage 2:</strong> AWS CLI (architecture-aware for ARM64/x86_64)</p><p><strong>Stage 3:</strong> Python packages (boto3, AWS SDK)</p><p><strong>Stage 4:</strong> Final runtime image</p><p>This approach keeps the final image lean (~500MB) while including everything we need.</p><h3>CloudFormation Template</h3><p>The infrastructure includes:</p><p><strong>S3 Buckets:</strong></p><ul><li>Agent storage with AES-256 encryption</li><li>Versioning enabled</li><li>Object Lock (30-day retention)</li><li>Access logging to separate bucket</li></ul><p><strong>IAM Role:</strong></p><ul><li>Least-privilege permissions</li><li>Account-scoped trust policy</li><li>S3 and Bedrock access only</li></ul><p><strong>Bedrock Agent:</strong></p><ul><li>Claude Sonnet 4.5 model</li><li>Custom personality instructions</li><li>Production alias for versioning</li></ul><h3>Security Features</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/628/1*nMv0hOwM0-egY7_QACcyEg.png" /></figure><h3>Part 3: Building the Container</h3><p>Start the Docker container:</p><pre>./start-dev-container.sh<br><br><br>You&#39;ll see:<br><br>Starting Avengers Development Container...<br><br>[+] Building... (2-3 minutes on first run)<br><br>Container started successfully!</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9WzANbhHIVeFUXkGs31MNw.png" /><figcaption>Expected output after starting the container</figcaption></figure><p>Enter the container:</p><pre>./enter-dev-container.sh<br><br><br>You&#39;re greeted with:<br><br>======================================<br>  AVENGERS BEDROCK DEV CONTAINER    <br>======================================<br><br>Working Directory: /workspace<br>Architecture: aarch64<br>Python: Python 3.10.12<br>AWS CLI: aws-cli/2.15.0<br><br>Quick Commands:<br>  awsid      - Show AWS identity<br>  awsregion  - Show AWS region<br>  ll         - List files</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sSFLP3Gpww0UKsDsdu80xQ.png" /><figcaption>Expected output once you are within your DEV container</figcaption></figure><h3>Part 4: Enable Claude Model Access</h3><p>Before deploying agents, we need to enable access to Claude models in AWS Bedrock.</p><h3>Request Model Access</h3><ol><li>Go to the <strong>AWS Console</strong> → <strong>Bedrock</strong> → <strong>Model access</strong></li><li>Click <strong>Manage model access</strong></li><li>Find the <strong>Anthropic</strong> section</li><li>Check <strong>Claude Sonnet 4.5</strong></li><li>Click <strong>Request model access</strong></li><li>Complete the use case form:</li></ol><ul><li><strong>Company:</strong> Personal Project (normally accepts this input)</li><li><strong>Industry:</strong> Technology</li><li><strong>Use case:</strong> “Educational project for learning AWS Bedrock and AI agents”</li></ul><p><strong><em>Approval is usually instant!</em></strong></p><h3>Verify Access</h3><p>Inside the container:</p><pre>aws bedrock-runtime invoke-model \<br>    --model-id &quot;us.anthropic.claude-sonnet-4-5-20250929-v1:0&quot; \<br>    --region us-east-1 \<br>    --body &#39;{&quot;anthropic_version&quot;:&quot;bedrock-2023-05-31&quot;,&quot;max_tokens&quot;:50,&quot;messages&quot;:[{&quot;role&quot;:&quot;user&quot;,&quot;content&quot;:&quot;Hi&quot;}]}&#39; \<br>    --content-type &quot;application/json&quot; \<br>    /tmp/test.json &amp;&amp; cat /tmp/test.json</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*q70F2RwA1E-bIWZ8CtB5ug.png" /><figcaption>Expected output after invoking the model</figcaption></figure><p>If you see a response, jump for excitement because you’re now ready to deploy!</p><h3>Part 5: Deploy Your First Agent — Iron Man</h3><p>Now for the exciting part…deploying Iron Man!</p><pre>./scripts/deploy-hero.sh <br><br><br>**What happens behind the scenes:**<br><br>1. **CloudFormation creates the stack** (3-5 minutes)<br>   - S3 buckets with encryption<br>   - IAM role with permissions<br>   - Bedrock agent configuration<br><br>2. **Agent preparation** (90 seconds)<br>   - Creates a versioned snapshot<br>   - Updates the production alias<br><br>3. **Automatic testing**<br>   - Sends a test message<br>   - Displays Iron Man&#39;s response</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XFQpD8AY3UaCvFfLGnBGrQ.png" /><figcaption>CloudFormation Stack completion</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GQKDuCcjXt0wZxzNxeBtvw.png" /><figcaption>CloudFormation resources that were created</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*egUm19XDlP6-Rh6rclAzEA.png" /><figcaption>First two buckets created for the Iron Man Agent</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/976/1*EMusU80ws9PZzOJ8mTJxig.png" /><figcaption>Test file sent to the logging bucket</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XGNoDyibnBQAOiCNvT8BRA.png" /><figcaption>Amazon Bedrock agent</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rjoOlP3WMXqdeu7SARKTmw.png" /><figcaption>CloudWatch Bedrock Dashboard</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hB3kC7d3i1Jt22eNXJvNfg.png" /><figcaption>CloudWatch Bedrock Metrics</figcaption></figure><p><strong>Beautiful!</strong> Iron Man is now live and responding in character.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0aGi3N-1jPTY7sRHT0O2sw.png" /><figcaption>Actual Response That You Should See!</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vTfZuZ_xVG9H6pIgxw9gWQ.png" /><figcaption>Shows you HOW to use the agent</figcaption></figure><h3>Part 6: Interacting with Your Agent</h3><h3>Single Questions</h3><p>Load the agent environment and ask a question:</p><pre>source /workspace/outputs/ironman-env.sh<br>python3 /workspace/scripts/test-agent.py &quot;How does the arc reactor work?&quot;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LgNVGrcxfWKdatKXu0znzg.png" /><figcaption>Agent response to the question above</figcaption></figure><h3>Interactive Chat</h3><p>Start a conversation:</p><pre>/workspace/scripts/chat.sh<br><br>======================================<br>  CHAT WITH IronMan<br>======================================</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wIkutWdgG_IcXNQfTnmpLw.png" /><figcaption>Sample chat message 1</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OJtYvO63axahghNJ2iWHJQ.png" /><figcaption>Sample chat message 2</figcaption></figure><p>Type exit to end the chat.</p><h3>Part 7: Deploy More Heroes</h3><p>The beauty of what we’ve created? We can deploy any character with one command, and follow the steps above if you’d like to chat with either character. <strong>It’s important to note</strong> that when you deploy a new character, a new CloudFormation stack is created, along with a bucket for the agent and a bucket for the logs.</p><h3>Thor: God of Thunder</h3><pre>./scripts/deploy-hero.sh thor Thor &quot;The God of Thunder from Asgard&quot;<br></pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*11tRgJhO4aaiDW7O82gx4w.png" /><figcaption>Successful creation of the Thor Agent</figcaption></figure><h3>Quick Sample Chat w/ Thor</h3><p>Feel free to chat away with the other Avengers!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rL2T2uZWFooxEvxDC8v3YQ.png" /><figcaption>Chatting with the Thor Agent</figcaption></figure><h3>Black Widow: Master Strategist</h3><pre>./scripts/deploy-hero.sh blackwidow BlackWidow &quot;a master spy and expert combatant. You are strategic, analytical, and always three steps ahead&quot;</pre><h3>Captain America: The Leader</h3><pre>./scripts/deploy-hero.sh captain CaptainAmerica &quot;a super soldier and natural leader from the 1940s. You inspire others and reference your experiences in World War II&quot;</pre><h3>Part 8: Behind the Scenes: How It Works</h3><p>Let’s peek under the hood at what makes this system tick. There were a LOT of moving pieces to get this deployed successfully, <strong><em>especially</em></strong> ensuring that we had very clean, and manageable scripts.</p><h3>CloudFormation Template</h3><p>The blueprint that automatically creates <strong>all of your AWS infrastructure</strong> with one command instead of clicking through the AWS Console for hours.</p><pre>AWSTemplateFormatVersion: &#39;2010-09-09&#39;<br>Description: &#39;Avengers AI Agent powered by Claude Sonnet 4.5 with Secure S3 Storage&#39;<br><br>Parameters:<br>  HeroName:<br>    Type: String<br>    Description: &#39;Hero name in lowercase (used for S3 bucket naming)&#39;<br>    AllowedPattern: &#39;^[a-z][a-z0-9-]*$&#39;<br>    ConstraintDescription: &#39;Must be lowercase letters, numbers, and hyphens only&#39;<br>  <br>  HeroDisplayName:<br>    Type: String<br>    Description: &#39;Display name for the hero (used for agent naming)&#39;<br>  <br>  HeroPersonality:<br>    Type: String<br>    Description: &#39;Personality description for the AI agent&#39;<br><br>Resources:<br>  AgentBucket:<br>    Type: AWS::S3::Bucket<br>    Properties:<br>      BucketName: !Sub &#39;${HeroName}-bedrock-agent-${AWS::AccountId}&#39;<br>      VersioningConfiguration:<br>        Status: Enabled<br>      ObjectLockEnabled: true<br>      ObjectLockConfiguration:<br>        ObjectLockEnabled: Enabled<br>        Rule:<br>          DefaultRetention:<br>            Mode: GOVERNANCE<br>            Days: 30<br>      PublicAccessBlockConfiguration:<br>        BlockPublicAcls: true<br>        BlockPublicPolicy: true<br>        IgnorePublicAcls: true<br>        RestrictPublicBuckets: true<br>      BucketEncryption:<br>        ServerSideEncryptionConfiguration:<br>          - ServerSideEncryptionByDefault:<br>              SSEAlgorithm: AES256<br>      LoggingConfiguration:<br>        DestinationBucketName: !Ref LoggingBucket<br>        LogFilePrefix: !Sub &#39;${HeroName}-logs/&#39;<br>      LifecycleConfiguration:<br>        Rules:<br>          - Id: TransitionToIA<br>            Status: Enabled<br>            Transitions:<br>              - TransitionInDays: 90<br>                StorageClass: STANDARD_IA<br>      Tags:<br>        - Key: Hero<br>          Value: !Ref HeroDisplayName<br>        - Key: Project<br>          Value: Avengers-Bedrock<br><br>  LoggingBucket:<br>    Type: AWS::S3::Bucket<br>    Properties:<br>      BucketName: !Sub &#39;${HeroName}-bedrock-logs-${AWS::AccountId}&#39;<br>      VersioningConfiguration:<br>        Status: Enabled<br>      PublicAccessBlockConfiguration:<br>        BlockPublicAcls: true<br>        BlockPublicPolicy: true<br>        IgnorePublicAcls: true<br>        RestrictPublicBuckets: true<br>      BucketEncryption:<br>        ServerSideEncryptionConfiguration:<br>          - ServerSideEncryptionByDefault:<br>              SSEAlgorithm: AES256<br>      LifecycleConfiguration:<br>        Rules:<br>          - Id: ExpireLogs<br>            Status: Enabled<br>            ExpirationInDays: 365<br><br>  AgentBucketPolicy:<br>    Type: AWS::S3::BucketPolicy<br>    Properties:<br>      Bucket: !Ref AgentBucket<br>      PolicyDocument:<br>        Version: &#39;2012-10-17&#39;<br>        Statement:<br>          - Sid: DenyInsecureTransport<br>            Effect: Deny<br>            Principal: &#39;*&#39;<br>            Action: &#39;s3:*&#39;<br>            Resource:<br>              - !GetAtt AgentBucket.Arn<br>              - !Sub &#39;${AgentBucket.Arn}/*&#39;<br>            Condition:<br>              Bool:<br>                &#39;aws:SecureTransport&#39;: &#39;false&#39;<br>          - Sid: AllowBedrockAgentAccess<br>            Effect: Allow<br>            Principal:<br>              AWS: !GetAtt BedrockAgentRole.Arn<br>            Action:<br>              - &#39;s3:GetObject&#39;<br>              - &#39;s3:PutObject&#39;<br>            Resource: !Sub &#39;${AgentBucket.Arn}/*&#39;<br><br>  BedrockAgentRole:<br>    Type: AWS::IAM::Role<br>    Properties:<br>      RoleName: !Sub &#39;${HeroDisplayName}-BedrockAgentRole&#39;<br>      AssumeRolePolicyDocument:<br>        Version: &#39;2012-10-17&#39;<br>        Statement:<br>          - Effect: Allow<br>            Principal:<br>              Service: bedrock.amazonaws.com<br>            Action: &#39;sts:AssumeRole&#39;<br>            Condition:<br>              StringEquals:<br>                &#39;aws:SourceAccount&#39;: !Ref &#39;AWS::AccountId&#39;<br>      ManagedPolicyArns:<br>        - &#39;arn:aws:iam::aws:policy/AmazonBedrockFullAccess&#39;<br>      Policies:<br>        - PolicyName: S3Access<br>          PolicyDocument:<br>            Version: &#39;2012-10-17&#39;<br>            Statement:<br>              - Effect: Allow<br>                Action:<br>                  - &#39;s3:GetObject&#39;<br>                  - &#39;s3:PutObject&#39;<br>                  - &#39;s3:ListBucket&#39;<br>                Resource:<br>                  - !GetAtt AgentBucket.Arn<br>                  - !Sub &#39;${AgentBucket.Arn}/*&#39;<br><br>  BedrockAgent:<br>    Type: AWS::Bedrock::Agent<br>    Properties:<br>      AgentName: !Sub &#39;${HeroDisplayName}-Agent&#39;<br>      AgentResourceRoleArn: !GetAtt BedrockAgentRole.Arn<br>      FoundationModel: &#39;us.anthropic.claude-sonnet-4-5-20250929-v1:0&#39;<br>      Instruction: !Sub |<br>        You are ${HeroDisplayName}, ${HeroPersonality}.<br>        <br>        Your mission is to assist users with their queries while staying completely in character.<br>        <br>        Character Guidelines:<br>        - Respond as ${HeroDisplayName} would, using their personality traits and speech patterns<br>        - Reference your abilities and background when relevant<br>        - Stay heroic, helpful, and true to your character<br>        - Use humor and wit appropriate to ${HeroDisplayName}<br>        <br>        Help users with their questions while making them smile and staying in character.<br>      IdleSessionTTLInSeconds: 600<br>      Description: !Sub &#39;AI Agent powered by ${HeroDisplayName} personality using Claude Sonnet 4.5&#39;<br><br>  BedrockAgentAlias:<br>    Type: AWS::Bedrock::AgentAlias<br>    Properties:<br>      AgentId: !GetAtt BedrockAgent.AgentId<br>      AgentAliasName: &#39;production-v1&#39;<br>      Description: &#39;Production alias for the Avengers agent&#39;<br><br>Outputs:<br>  AgentId:<br>    Description: &#39;Bedrock Agent ID&#39;<br>    Value: !GetAtt BedrockAgent.AgentId<br>    Export:<br>      Name: !Sub &#39;${HeroDisplayName}-AgentId&#39;<br>  <br>  AgentAliasId:<br>    Description: &#39;Agent Alias ID&#39;<br>    Value: !GetAtt BedrockAgentAlias.AgentAliasId<br>    Export:<br>      Name: !Sub &#39;${HeroDisplayName}-AliasId&#39;<br>  <br>  AgentBucketName:<br>    Description: &#39;Secure S3 Bucket&#39;<br>    Value: !Ref AgentBucket<br>  <br>  LoggingBucketName:<br>    Description: &#39;Logging Bucket&#39;<br>    Value: !Ref LoggingBucket<br>  <br>  AgentRoleArn:<br>    Description: &#39;Agent IAM Role ARN&#39;<br>    Value: !GetAtt BedrockAgentRole.Arn</pre><h3>The Deployment Script</h3><p>The deploy-hero.sh script orchestrates everything:</p><pre>#!/bin/bash<br>set -e<br><br>HERO_NAME=${1}<br>HERO_DISPLAY=${2}<br>HERO_PERSONALITY=${3}<br><br># 1. Checks for an existing stack, deletes it if found<br># 2. Creates a CloudFormation stack<br># 3. Waits for completion<br># 4. Extracts outputs (Agent ID, Alias ID, Bucket)<br># 5. Saves the environment file<br># 6. Prepares the agent (creates version snapshot)<br># 7. Updates the alias to point to the &#39;correct&#39; version<br># 8. Runs automatic tests</pre><p><strong>Key insight:</strong> The script is idempotent. You can run it multiple times, but be sure that you’re doing it in a safe manner.</p><h3>The Python Test Script</h3><pre>def test_agent(agent_id, alias_id, input_text):<br>    client = boto3.client(&#39;bedrock-agent-runtime&#39;)<br>    <br>    # Invoke agent with streaming response<br>    response = client.invoke_agent(<br>        agentId=agent_id,<br>        agentAliasId=alias_id,<br>        sessionId=f&quot;session-{timestamp}&quot;,<br>        inputText=input_text,<br>        enableTrace=False<br>    )<br>    <br>    # Process streaming chunks<br>    output_text = &quot;&quot;<br>    for event in response.get(&#39;completion&#39;, []):<br>        if &#39;chunk&#39; in event:<br>            output_text += event[&#39;chunk&#39;][&#39;bytes&#39;].decode(&#39;utf-8&#39;)<br>    <br>    return output_text</pre><p><strong>Key features:</strong></p><ul><li>Retry logic with exponential backoff</li><li>Streaming response handling</li><li>Clear error messages</li></ul><h3>CloudFormation Template Highlights</h3><p>** The CloudFormation Template was easily one of my more <em>challenging</em> components to get correct for this project as the formatting was just not letting me be great**</p><pre>BedrockAgent:<br>  Type: AWS::Bedrock::Agent<br>  Properties:<br>    AgentName: !Sub &#39;${HeroDisplayName}-Agent&#39;<br>    FoundationModel: &#39;us.anthropic.claude-sonnet-4-5-20250929-v1:0&#39;<br>    Instruction: !Sub |<br>      You are ${HeroDisplayName}, ${HeroPersonality}.<br>      <br>      Stay completely in character.<br>      Reference your abilities and background.<br>      Help users while making them smile.</pre><p>The personality is injected directly into the system prompt!</p><h3>Part 9: Lessons Learned</h3><p>Building this project taught me several valuable lessons:</p><h3>1. Apple Silicon &amp; Inference Profiles Requires Attention</h3><p><strong>Problem:</strong> Docker images built for x86_64 fail on ARM64 with cryptic errors, and direct model invocation for Claude Sonnet 4.5 was a no-go.</p><p><strong>Solution:</strong> Multi-stage Dockerfile with architecture detection, and switching to the inference profile format:</p><pre>RUN ARCH=$(uname -m) &amp;&amp; \<br>    if [ &quot;$ARCH&quot; = &quot;aarch64&quot; ]; then \<br>        curl &quot;...-aarch64.zip&quot; -o &quot;awscliv2.zip&quot;; \<br>    else \<br>        curl &quot;...-x86_64.zip&quot; -o &quot;awscliv2.zip&quot;; \<br>    fi<br><br><br>## Inference Profiles are Required<br><br>**Problem:** Direct model invocation fails with Claude Sonnet 4.5<br><br>**Solution:** Use the inference profile format:<br><br>us.anthropic.claude-sonnet-4-5-20250929-v1:0<br><br><br>Not the foundation model ID:<br><br>anthropic.claude-sonnet-4-5-20250929-v1:0</pre><h3>2. Agent Aliases Need Versions</h3><p><strong>Problem:</strong> Alias creation succeeds but invocations fail</p><p><strong>Solution:</strong></p><ol><li>Prepare the agent (creates a snapshot)</li><li>Get the version number</li><li>Update the alias to point to that version</li><li>Never use DRAFT in production</li></ol><h3>3. Model Access is Not Automatic</h3><p><strong>Problem:</strong> “resourceNotFoundException” even though model exists. I saw this message ~5 times before I figured out my issue 😭🥴.</p><p><strong>Solution:</strong> Submit the Anthropic use case form in the AWS Console. Approval is instant but <em>required</em>.</p><h3>Part 10: Cost Management</h3><p>Let’s talk about actual costs as I was very surprised that this deployment didn’t cost $100,000 🥹🤣.</p><h3>Pricing Breakdown</h3><p><strong>AWS Bedrock (Claude Sonnet 4.5):</strong></p><ul><li>Input: $0.003 per 1K tokens</li><li>Output: $0.015 per 1K tokens</li></ul><p><strong>S3 Storage:</strong></p><ul><li>Standard: $0.023 per GB/month</li><li>Requests: $0.0004 per 1K PUT requests</li></ul><p><strong>Example costs:</strong></p><ul><li>100 conversations/day (avg 500 tokens each): ~$2.25/month</li><li>S3 storage (minimal): ~$0.05/month</li><li><strong>Total: Under $3/month</strong> for ‘moderate’ usage</li></ul><h3>Cost Optimization Tips</h3><ol><li><strong>Use lifecycle policies</strong> (already configured)</li></ol><ul><li>Transitions to Infrequent Access after 90 days</li><li>Logs expire after 365 days</li></ul><p><strong>2. Monitor usage (metrics will take some time to appear, so no need to panic!):</strong></p><pre>aws cloudwatch get-metric-statistics \<br>       --namespace AWS/Bedrock \<br>       --metric-name InvocationCount</pre><pre># List your agent invocations (if any)<br>aws bedrock-agent list-agents \<br>    --region us-east-1 \<br>    --query &#39;agentSummaries[*].[agentName,agentId,agentStatus]&#39; \<br>    --output table</pre><pre># List your logging bucket<br>aws s3 ls s3://ironman-bedrock-logs-YOUR_ACCOUNT_ID/<br><br># Or for Thor<br>aws s3 ls s3://thor-bedrock-logs-YOUR_ACCOUNT_ID/</pre><pre># View today&#39;s Bedrock costs (appears within hours)<br>aws ce get-cost-and-usage \<br>    --time-period Start=$(date +%Y-%m-01),End=$(date -d tomorrow +%Y-%m-%d) \<br>    --granularity DAILY \<br>    --metrics UnblendedCost \<br>    --filter file://&lt;(echo &#39;{&quot;Dimensions&quot;:{&quot;Key&quot;:&quot;SERVICE&quot;,&quot;Values&quot;:[&quot;Amazon Bedrock&quot;]}}&#39;) \<br>    --region us-east-1</pre><p><strong>3. Set billing alerts:</strong></p><ul><li>AWS Console → Billing → Budgets</li><li>Set an alert at $5/month</li></ul><h3>Part 11: Cleanup</h3><p>When you’re done experimenting, always be sure to clean up resources that you no longer require, unless you’d like a surprise bill 😊.</p><h3>Delete All Agents</h3><pre># Inside the container<br>for STACK in $(aws cloudformation list-stacks \<br>    --stack-status-filter CREATE_COMPLETE \<br>    --query &#39;StackSummaries[?contains(StackName, `avengers`)].StackName&#39; \<br>    --output text); do<br>    aws cloudformation delete-stack --stack-name $STACK<br>done</pre><h3>Stop the Container</h3><pre># Exit the container<br>exit</pre><pre># Stop and remove the container<br>./stop-dev-container.sh</pre><p><strong>Costs after cleanup:</strong> $0 (S3 buckets are deleted)</p><h3>Conclusion</h3><p>While this tutorial uses Marvel characters for fun, the underlying architecture <em>can</em> be production-ready, with a few additional security tweaks as necessary. We’ve built a complete AI agent deployment system from scratch, with a little pain in between, but we got it done!! You now have:</p><pre>**A Containerized development environment** that works on any Mac<br>**Infrastructure-as-Code** for reproducible deployments  <br>**Production-ready security** with encryption and access controls<br>**Scalable architecture** that handles multiple agents<br>**Cost-effective solution** at under $3/month<br></pre><p>If you’ve enjoyed this tutorial, stay tuned as there is definitely more to come 🚀🔒!!</p><p>Follow me on LinkedIn @katoria-henry-2018! ✅</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8809ed4b722a" width="1" height="1" alt=""><hr><p><a href="https://levelup.gitconnected.com/building-ai-powered-marvel-avengers-chatbots-with-aws-bedrock-and-claude-sonnet-4-5-8809ed4b722a">Building AI-Powered Marvel Avengers Chatbots with AWS Bedrock and Claude Sonnet 4.5</a> was originally published in <a href="https://levelup.gitconnected.com/">Level Up Coding</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Docker Multi-Stage FastAPI Tutorial]]></title>
            <link>https://python.plainenglish.io/docker-multi-stage-fastapi-tutorial-cb0d97b6d274?source=rss-6e1dbb973ea1------2</link>
            <guid isPermaLink="false">https://medium.com/p/cb0d97b6d274</guid>
            <category><![CDATA[docker]]></category>
            <category><![CDATA[docker-scout]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[automation]]></category>
            <category><![CDATA[vulnerability-management]]></category>
            <dc:creator><![CDATA[Katoria Henry]]></dc:creator>
            <pubDate>Tue, 25 Feb 2025 14:16:01 GMT</pubDate>
            <atom:updated>2025-02-25T14:16:01.822Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/379/1*MltGUGoLKjszeLgM56UkJw.png" /><figcaption>Gnerated via a Custom Python Script</figcaption></figure><p>Docker has revolutionized the way developers build, ship, and run applications, making containerization a core part of modern DevOps workflows. While many tutorials focus on simple Flask applications, this guide takes things to the <strong>next level</strong> by showcasing a fully automated, production-ready <strong>multi-stage Docker build</strong> using <strong>FastAPI</strong>. But that’s just the beginning. We’ll go beyond basic containerization by leveraging <strong>Python automation</strong> to build, deploy, and verify our application — all while ensuring a minimal, optimized Docker image.</p><p>Additionally, we’ll integrate <strong>Docker Scout</strong> for <strong>manual vulnerability scanning</strong>, adding a security-focused layer to our workflow. This <strong>end-to-end tutorial</strong> is designed to impress, demonstrating best practices for <strong>efficient container builds, dependency management, and automation</strong>. Whether you’re a Solutions Architect<strong>, a DevOps engineer, or just looking to refine your containerization skills</strong>, this tutorial will equip you with practical techniques to streamline your Docker-based development process. 🚀</p><p><strong>Key Features</strong></p><p>✅ <strong>Automated Docker build, run, and status verification using Python<br></strong>✅ <strong>Optimized multi-stage Dockerfile</strong> (<em>minimizing layers and improving caching efficiency</em>)<br>✅ <strong>FastAPI-based application with multiple endpoints and external API calls<br></strong>✅ <strong>Manual Docker Scout scanning</strong> (ensuring security best practices)</p><p><strong>Prerequisites for This Tutorial</strong></p><p>Before diving into this tutorial, ensure you have the following prerequisites set up on your system:</p><h4>📝System Requirements</h4><ul><li>A <strong>macOS, Linux, or Windows machine</strong> with Docker installed.</li><li>At least <strong>4GB of RAM</strong> to run containers smoothly (or sufficient RAM).</li></ul><h4>📝 Installed Software</h4><ul><li><strong>Docker Desktop</strong> — Install from Docker’s official site</li><li><strong>Python 3.8+</strong></li></ul><p>👩🏽‍💻Set Up a Python Virtual Environment</p><p>🐍Install Required Python Packages</p><p>🚢Verify Docker Installation</p><p>🚢Install Docker Scout (Optional)</p><h3><strong>Build, Automate, &amp; Scan Your Dockerized Application Like A Pro</strong></h3><h4>Application-Specific Requirements</h4><p>We’ll start things off by first installing FastAPI in our python environment:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*xsnBWmH1Rp6rU4EC" /></figure><p>To ensure we have the necessary libraries that will be needed for our deployment, let’s create our requirements.txt file next:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/984/0*_BPeGcYC4t6PJ7aP" /></figure><p>Because we’re using FastAPI as part of this build, we’ll need to create an app.py file that includes the core application logic that will run inside the Docker container. This logic will define the FastAPI-based web service, allowing users to send HTTP requests and receive responses. This is the <strong>actual application</strong> we are containerizing, so it’s crucial to verify that it works <strong>before</strong> packaging it into a Docker image.</p><p>Before building and running the application inside a container, testing it <strong>locally</strong> ensures:</p><ol><li><strong>The code is functional</strong> — This confirms that the FastAPI app is correctly set up and can handle requests.</li><li><strong>Dependencies are installed</strong> — If the app runs outside Docker, it means Python and required packages (e.g., uvicorn, requests) are installed correctly.</li><li><strong>Easier Debugging</strong> — Running the app locally helps catch <strong>syntax errors, import issues, or missing dependencies</strong> before introducing Docker.</li><li><strong>Faster Iteration</strong> — Fixing issues outside Docker saves time compared to repeatedly building and running containers.</li></ol><pre>from fastapi import FastAPI, HTTPException # type: ignore<br>import requests # type: ignore<br><br>app = FastAPI()<br><br>@app.get(&quot;/&quot;)<br>def read_root():<br>    return {&quot;message&quot;: &quot;Hello, Docker Captains! This is a multi-stage Docker build project.&quot;}<br><br>@app.get(&quot;/status&quot;)<br>def get_status():<br>    return {&quot;status&quot;: &quot;Running&quot;, &quot;version&quot;: &quot;1.0.0&quot;}<br><br>@app.get(&quot;/external-api&quot;)<br>def fetch_external_api():<br>    &quot;&quot;&quot;Fetch data from an external API to demonstrate network requests.&quot;&quot;&quot;<br>    try:<br>        response = requests.get(&quot;https://api.github.com/repos/docker/docker&quot;)<br>        return response.json()<br>    except requests.RequestException:<br>        raise HTTPException(status_code=500, detail=&quot;External API request failed.&quot;)</pre><p>We can execute a local dry run of the app.py file to confirm that we can in fact get a health status check and fetch data from an external API:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*yjZeJV2b0VS4q_AY" /><figcaption>Local Script Execution</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/962/0*Eudzl-qr3majqSGg" /><figcaption>Status check: <a href="http://localhost:8000/status">http://localhost:8000/status</a></figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*lCDkAlVIFn3Dem2X" /><figcaption>External API Verification: <a href="http://localhost:8000/external-api">http://localhost:8000/external-api</a></figcaption></figure><h4>Crafting Our Dockerfile &amp; Python Script</h4><p>This <strong>Dockerfile</strong> is designed for efficiency and security using a <strong>multi-stage build</strong> approach. It installs dependencies in a temporary builder stage and only copies the necessary files into the final production image, keeping it <strong>lightweight and optimized</strong>. By structuring the layers strategically, it <strong>leverages caching</strong> to speed up builds while ensuring that unnecessary files, such as build tools and caches, are removed. This results in a <strong>smaller, faster, and more secure</strong> container that is ready for production use. 🚀</p><pre># --- Base Image (Builder Stage) ---<br>    FROM python:3.11-slim AS builder<br>    WORKDIR /app<br>    <br>    # Install dependencies in a single RUN statement for efficient layer caching<br>    RUN apt-get update &amp;&amp; \<br>        apt-get install -y gcc curl &amp;&amp; \<br>        rm -rf /var/lib/apt/lists/*<br>    <br>    # Copy requirements and install the required dependencies<br>    COPY requirements.txt .<br>    RUN pip install --no-cache-dir --prefix=/install -r requirements.txt<br>    <br>    # --- Final Production Image ---<br>    FROM python:3.11-slim<br>    WORKDIR /app<br>    <br>    # Copy built dependencies from the builder stage<br>    COPY --from=builder /install /usr/local<br>    <br>    # Ensure the correct path is used for installed Python packages<br>    ENV PYTHONPATH=/usr/local/lib/python3.11/site-packages<br>    <br>    # Copy the necessary application files<br>    COPY . .<br>    <br>    # Expose the desire port and define the entry point<br>    EXPOSE 8000<br>    CMD [&quot;uvicorn&quot;, &quot;app:app&quot;, &quot;--host&quot;, &quot;0.0.0.0&quot;, &quot;--port&quot;, &quot;8000&quot;]</pre><p>The <strong>Python script (</strong><strong>deploy.py)</strong> automates the entire Docker workflow, eliminating the need for manual commands. It <strong>builds the Docker image, runs the container, and verifies the application’s status</strong> to ensure a seamless deployment. This approach <strong>streamlines development, reduces human error, and saves time</strong>, making it easier to manage containerized applications efficiently. Instead of manually executing multiple Docker commands, this script <strong>handles everything with a single execution</strong>, ensuring consistency across deployments.</p><h4>Key Features of deploy.py</h4><p>🏁<strong>Automated Docker Build</strong> — Builds the FastAPI container with optimized caching<br>🏁<strong>Container Deployment</strong> — Runs the container with the correct ports and settings<br>🏁<strong>Health Check</strong> — Calls the /status endpoint to confirm the application is running</p><p>🏁<strong>Error Handling</strong> — Provides meaningful error messages if any step fails<br>🏁 <strong>Manual Cleanup Option</strong> — Leaves the container running for <strong>Docker Scout vulnerability scanning</strong></p><p>This script <strong>enhances developer productivity</strong> by making Dockerized deployments <strong>faster, repeatable, and more reliable</strong>!</p><pre>import subprocess<br>import time<br>import requests # type: ignore<br><br>def run_command(command):<br>    &quot;&quot;&quot;Runs a shell command with error handling.&quot;&quot;&quot;<br>    try:<br>        subprocess.run(command, check=True)<br>    except subprocess.CalledProcessError as e:<br>        print(f&quot;Error executing {command}: {e}&quot;)<br>        exit(1)<br><br>def build_image():<br>    print(&quot;\n🚀 Building the Docker image...&quot;)<br>    run_command([&quot;docker&quot;, &quot;build&quot;, &quot;-t&quot;, &quot;fastapi-multi-stage&quot;, &quot;.&quot;])<br><br>def run_container():<br>    print(&quot;\n🚀 Running the container...&quot;)<br>    run_command([&quot;docker&quot;, &quot;run&quot;, &quot;-d&quot;, &quot;-p&quot;, &quot;8000:8000&quot;, &quot;--name&quot;, &quot;fastapi-app&quot;, &quot;fastapi-multi-stage&quot;])<br>    time.sleep(3)<br><br>def check_status():<br>    &quot;&quot;&quot;Check if the container is running by calling the /status endpoint.&quot;&quot;&quot;<br>    url = &quot;http://localhost:8000/status&quot;<br>    try:<br>        response = requests.get(url)<br>        if response.status_code == 200:<br>            print(&quot;\n✅ Application is running successfully!&quot;)<br>            print(f&quot;Response: {response.json()}&quot;)<br>        else:<br>            print(f&quot;\n⚠️ Unexpected response: {response.status_code}&quot;)<br>    except requests.exceptions.RequestException:<br>        print(&quot;\n❌ Application failed to start. Check logs.&quot;)<br><br>if __name__ == &quot;__main__&quot;:<br>    build_image()<br>    run_container()<br>    check_status()<br><br>    print(&quot;\n📸 The container is running and ready for Docker Scout scanning.&quot;)<br>    print(&quot;🛑 To stop and remove the container, run: docker stop fastapi-app &amp;&amp; docker rm fastapi-app&quot;)</pre><p>And if you execute the script by running python deploy.py, this is what you should see:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*jBAWH6t76N-9h6Um" /><figcaption>The Docker image being built</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*nYX9Ip9VoTsYfg3-" /><figcaption>Confirmation that the Container &amp; App are up and running</figcaption></figure><p>We can verify that the application is in fact up and running by navigating to <a href="http://localhost:8000,">http://localhost:8000,</a> and here, we should see our little Docker Captain’s message (I’m a new Docker Captain btw ☺️):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*SRsH2yT0V-So69uV" /></figure><p>Assuming that you have Docker Desktop up and running, you can also verify your image that was created, container, logs, docker build, and so forth in the tool directly:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*4hKcftbD8mAEUKjb" /><figcaption>FastAPI Docker Image that was created</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*G9APC3l56j1UDNo_" /><figcaption>Docker Build source details</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*PIy9BOQQGRZwzIMX" /><figcaption>Dependencies &amp; Build results</figcaption></figure><h3>Vulnerability Management: Why Is This Important for Docker Images?</h3><p>Containerized applications are only as secure as the images they are built from, making <strong>vulnerability management</strong> a critical aspect of <strong>Docker security</strong>. Since Docker images often include <strong>base OS layers, dependencies, and third-party libraries</strong>, any unpatched vulnerabilities in these components can introduce security risks, <strong>potentially exposing applications to exploits, malware, and data breaches</strong>. Regularly scanning and updating images helps <strong>mitigate these risks, reduce attack surfaces, and ensure compliance with security best practices</strong>.</p><p><strong>Docker Scout</strong> plays a vital role in this process by providing <strong>automated vulnerability analysis</strong> for container images. It helps developers quickly <strong>identify known security issues</strong>, analyze Common Vulnerabilities and Exposures (<strong>CVEs</strong>), and make informed decisions about <strong>remediating risks</strong> before deploying containers to production. By integrating <strong>Docker Scout</strong> into the development workflow, teams can proactively manage vulnerabilities, ensuring that their Docker images remain <strong>secure, reliable, and production-ready.</strong></p><p>If your build was successful, you would have noticed a “What’s Next” blurb, which directs you to take a quick view of the Docker Scout summary of the image vulnerabilities and recommendations by simply running docker scout quickview fastapi-multi-stage. It <strong>scans for known vulnerabilities (CVEs)</strong> and gives an <strong>overview of critical, high, medium, and low-risk issues</strong> found in the image layers. This command is useful for <strong>fast vulnerability assessments</strong>, allowing teams to <strong>identify risks early in the development cycle</strong> without diving into full detailed reports.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*apj7POkgs4t6D_V7" /></figure><p>To actually dive deeper into the CVEs that were identified, you would run docker scout cves fastapi-multi-stage. This command performs a <strong>detailed vulnerability scan</strong> on a Docker image, listing all detected <strong>Common Vulnerabilities and Exposures (CVEs)</strong> along with their severity levels. Unlike quickview, which provides a summary, this command gives <strong>in-depth insights</strong> into each vulnerability, including affected packages, CVE identifiers, and available fixes. It helps developers <strong>identify and prioritize security risks</strong>, ensuring that container images remain <strong>secure and compliant</strong> before deployment.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*99V9hdT33Hddu0WY" /><figcaption>Detection of Vulnerabilities</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*QcClnCn0EV9DSboj" /><figcaption>Additional Vulnerability Information</figcaption></figure><p>The docker scout recommendations command provides <strong>actionable security insights</strong> by suggesting <strong>remediation steps</strong> for vulnerabilities found in a Docker image. Unlike basic vulnerability scans, this command goes further by <strong>identifying safer base image alternatives</strong>, recommending <strong>updated dependencies</strong>, and offering <strong>best practices</strong> to reduce security risks. This is beneficial because it helps developers <strong>proactively fix issues</strong>, minimize the attack surface, and ensure <strong>more secure and resilient containers</strong> before deployment. To see the recommendations, you would simply run docker scout recommendations fastapi-multi-stage:latest:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*GlOcZbyf6L_LS5tk" /><figcaption>Shows which image was targeted for recommendations</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*LxxKqHPlNkBF4rL9" /><figcaption>Actual Recommendations</figcaption></figure><h3>Closing Thoughts</h3><p>Mastering <strong>Docker automation, multi-stage builds, and vulnerability management</strong> is essential for building <strong>efficient, secure, and production-ready containers</strong>. By leveraging <strong>Python automation</strong> for deployment and <strong>Docker Scout</strong> for security analysis, this tutorial streamlines the entire container lifecycle — <strong>from development to security hardening</strong>. Whether you’re a <strong>DevOps engineer, a security-conscious developer, or a Docker enthusiast</strong>, integrating these best practices will help you <strong>build smarter, deploy faster, and secure your applications more effectively</strong>.</p><p>Now that you have the tools to automate and scan your images, the next step is to <strong>apply these techniques to your own projects</strong> and elevate your container security game. 🚀💡 <strong>What’s the first image you’ll scan with Docker Scout?</strong></p><p>Feel free to follow me on <a href="https://www.linkedin.com/in/katoria-henry-2018">LinkedIn</a> for more content, and find your next Docker Captain <a href="https://www.docker.com/community/captains/#captains">here</a>!</p><p>💫 <strong>Bonus</strong>💫: Check out the python script that I created to generate the diagram for this tutorial:</p><pre>import matplotlib.pyplot as plt<br>import networkx as nx<br><br>def draw_docker_workflow():<br>    # Define graph<br>    G = nx.DiGraph()<br><br>    # Define nodes with labels<br>    nodes = {<br>        &quot;start&quot;: &quot;Start Project\n(Set Up Python Env)&quot;,<br>        &quot;write_app&quot;: &quot;Write FastAPI App\n(app.py)&quot;,<br>        &quot;test_locally&quot;: &quot;Test Locally\n(uvicorn app:app)&quot;,<br>        &quot;create_dockerfile&quot;: &quot;Create Multi-Stage Dockerfile&quot;,<br>        &quot;run_deploy&quot;: &quot;Run deploy.py\n(Auto-Build &amp; Run Container)&quot;,<br>        &quot;check_status&quot;: &quot;Check App Status\n(via API Endpoint)&quot;,<br>        &quot;scan_image&quot;: &quot;Scan Image with\nDocker Scout&quot;,<br>        &quot;review_remediate&quot;: &quot;Review &amp; Remediate\nUsing Docker Scout&quot;,<br>        &quot;deploy_production&quot;: &quot;Deploy Secure Container\nto Production&quot;<br>    }<br><br>    # Define edges<br>    edges = [<br>        (&quot;start&quot;, &quot;write_app&quot;),<br>        (&quot;write_app&quot;, &quot;test_locally&quot;),<br>        (&quot;test_locally&quot;, &quot;create_dockerfile&quot;),<br>        (&quot;create_dockerfile&quot;, &quot;run_deploy&quot;),<br>        (&quot;run_deploy&quot;, &quot;check_status&quot;),<br>        (&quot;check_status&quot;, &quot;scan_image&quot;),<br>        (&quot;scan_image&quot;, &quot;review_remediate&quot;),<br>        (&quot;review_remediate&quot;, &quot;deploy_production&quot;)<br>    ]<br><br>    # Add nodes and edges to graph<br>    G.add_nodes_from(nodes.keys())<br>    G.add_edges_from(edges)<br><br>    # Node positions (layered top to bottom)<br>    pos = {<br>        &quot;start&quot;: (0, 8),<br>        &quot;write_app&quot;: (0, 7),<br>        &quot;test_locally&quot;: (0, 6),<br>        &quot;create_dockerfile&quot;: (0, 5),<br>        &quot;run_deploy&quot;: (0, 4),<br>        &quot;check_status&quot;: (0, 3),<br>        &quot;scan_image&quot;: (0, 2),<br>        &quot;review_remediate&quot;: (0, 1),<br>        &quot;deploy_production&quot;: (0, 0)<br>    }<br><br>    # Node colors based on function<br>    node_colors = {<br>        &quot;start&quot;: &quot;lightblue&quot;,<br>        &quot;write_app&quot;: &quot;#fdf5c9&quot;,<br>        &quot;test_locally&quot;: &quot;lightgreen&quot;,<br>        &quot;create_dockerfile&quot;: &quot;#fdf5c9&quot;,<br>        &quot;run_deploy&quot;: &quot;lightgreen&quot;,<br>        &quot;check_status&quot;: &quot;lightcoral&quot;,<br>        &quot;scan_image&quot;: &quot;#fdf5c9&quot;,<br>        &quot;review_remediate&quot;: &quot;lightgreen&quot;,<br>        &quot;deploy_production&quot;: &quot;lightblue&quot;<br>    }<br><br>    # Draw nodes<br>    plt.figure(figsize=(6, 10))<br>    nx.draw(G, pos, labels=nodes, with_labels=True, node_size=4000, font_size=10, <br>            node_color=[node_colors[n] for n in G.nodes()], edge_color=&#39;black&#39;, font_weight=&#39;bold&#39;, arrows=True)<br>    <br>    # Display the diagram<br>    plt.title(&quot;Docker Project Workflow&quot;)<br>    plt.show()<br><br># Call the function to generate the diagram<br>draw_docker_workflow()</pre><h3>Thank you for being a part of the community</h3><p><em>Before you go:</em></p><ul><li>Be sure to <strong>clap</strong> and <strong>follow</strong> the writer ️👏<strong>️️</strong></li><li>Follow us: <a href="https://x.com/inPlainEngHQ"><strong>X</strong></a> | <a href="https://www.linkedin.com/company/inplainenglish/"><strong>LinkedIn</strong></a> | <a href="https://www.youtube.com/channel/UCtipWUghju290NWcn8jhyAw"><strong>YouTube</strong></a> | <a href="https://newsletter.plainenglish.io/"><strong>Newsletter</strong></a> | <a href="https://open.spotify.com/show/7qxylRWKhvZwMz2WuEoua0"><strong>Podcast</strong></a></li><li><a href="https://cofeed.app/"><strong>Check out CoFeed, the smart way to stay up-to-date with the latest in tech</strong></a> <strong>🧪</strong></li><li><a href="https://differ.blog/"><strong>Start your own free AI-powered blog on Differ</strong></a> 🚀</li><li><a href="https://discord.gg/in-plain-english-709094664682340443"><strong>Join our content creators community on Discord</strong></a> 🧑🏻‍💻</li><li>For more content, visit <a href="https://plainenglish.io/"><strong>plainenglish.io</strong></a> + <a href="https://stackademic.com/"><strong>stackademic.com</strong></a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cb0d97b6d274" width="1" height="1" alt=""><hr><p><a href="https://python.plainenglish.io/docker-multi-stage-fastapi-tutorial-cb0d97b6d274">Docker Multi-Stage FastAPI Tutorial</a> was originally published in <a href="https://python.plainenglish.io/">Python in Plain English</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Creating a Secure, Encrypted AMI Using Boto3, Python, and GitHub Actions]]></title>
            <link>https://python.plainenglish.io/creating-a-secure-encrypted-ami-using-boto3-python-and-github-actions-22dc721bd985?source=rss-6e1dbb973ea1------2</link>
            <guid isPermaLink="false">https://medium.com/p/22dc721bd985</guid>
            <category><![CDATA[github-actions]]></category>
            <category><![CDATA[pipeline-automation]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[boto3]]></category>
            <dc:creator><![CDATA[Katoria Henry]]></dc:creator>
            <pubDate>Mon, 13 Jan 2025 08:17:03 GMT</pubDate>
            <atom:updated>2025-01-13T08:17:03.589Z</atom:updated>
            <content:encoded><![CDATA[<p>Creating a Secure, Encrypted AMI Using Boto3, Python, and GitHub Actions</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*d0K5oNfmEkyPFhNnXTOaHw.png" /><figcaption>Image generated via eraser.io</figcaption></figure><p>This tutorial is designed to help you create a <strong>secure, encrypted Amazon Machine Image (AMI)</strong> using Python and the AWS SDK for Python (Boto3), with a robust GitHub Actions pipeline to validate and enhance the process. Each step is broken down for <strong>junior developers</strong> to understand the logic, purpose, and value of the code and pipeline. By the end of this very simple, yet, valuable tutorial, you will know how to run the script and the pipeline locally or trigger the workflow in GitHub directly.</p><p>An <strong>Amazon Machine Image (AMI)</strong> is a pre-configured virtual machine template in AWS that includes an operating system, application code, storage volume definitions, and permissions. It serves as a blueprint for launching <strong>EC2 instances</strong>, enabling users to replicate pre-configured environments quickly and consistently. AMIs are critical for ensuring consistency across instances, streamlining automation, supporting scalability, and reducing manual configuration time. They are highly customizable and tailored to specific workloads, making them essential for efficient deployment and operational management.</p><p>Encrypting an AMI before sharing it with another account is vital for maintaining data security and compliance. AMIs often contain sensitive application configurations, proprietary software, or confidential data. Encryption ensures that the contents of the AMI are protected from unauthorized access and can only be accessed by accounts or users with the proper decryption key. This practice is particularly important for meeting regulatory compliance requirements, such as GDPR or HIPAA, which mandate encryption for stored or shared data</p><p>In this tutorial, you’ll:</p><ul><li><strong>Create a secure AMI</strong> with encrypted volumes using a custom KMS key.</li><li><strong>Validate AMI ownership and sharing permissions</strong>.</li><li><strong>Implement robust automation using GitHub Actions</strong> for end-to-end validation.</li></ul><p>You’ll also learn how to <strong>dry run</strong> the workflow locally and <strong>visualize it in GitHub</strong>.</p><h3>Pre-requisites</h3><p>Install the following tools:</p><ul><li><strong>Boto3 (AWS SDK)</strong></li><li><strong>Python</strong> (3.9+)</li><li><strong>AWS CLI</strong>, configured with YOUR credentials</li><li><strong>Git</strong> and <strong>GitHub CLI</strong> (optional for remote runs)</li></ul><h3>Directory Structure</h3><p>Here’s the structure of the project, which can be forked from this <a href="https://github.com/theCaptN21/secure-ec2-ami-deployment">repo</a>, if you’d like to give it a go on your end:</p><pre>.<br>├── .github/<br>│   └── workflows/<br>│       └── secure_ami.yml  # GitHub Actions workflow<br>├── requirements.txt         # Python dependencies<br>├── secure_ami.py            # Python script for secure AMI creation<br>├── .gitignore               # Files to ignore in version control<br>├── README.md                # Project documentation</pre><p><strong>Setting Up Your Environment</strong></p><p>In addition to the pre-requisites listed above, it is <em>highly recommended</em> that you consider setting up a <strong>virtual python environment</strong> as well. This would be extremely helpful as we’ll be leveraging the Boto3 SDK and we’ll want to perform a dry run locally. To ensure that you won’t encounter any potential errors locally, consider setting the following environment variables, which are vital pieces within the script that we will execute:</p><pre>export AWS_REGION=&quot;your-desired-region&quot;<br>export AWS_ACCESS_KEY_ID=&quot;your-access-key-id&quot;<br>export AWS_SECRET_ACCESS_KEY=&quot;your-secret-access-key&quot;<br>export INSTANCE_TYPE=&quot;t2.micro&quot;<br>export BASE_AMI_ID=&quot;ami-xxxxx&quot; # Replace this with a valid AMI ID<br>export KEY_PAIR_NAME=&quot;your-key-pair&quot;<br>export SECURITY_GROUP_NAME=&quot;your-security-group&quot;<br>export MY_PUBLIC_IP=&quot;your-public-ip&quot; # E.g., &quot;203.0.113.0&quot;</pre><p>You’ll need a requirements.txt file that includes the following libraries collectively, to enable the secure creation and management of AWS resources, at a minimum:</p><pre>boto3==1.26.160<br>paramiko==3.2.0<br>cryptography==41.0.3</pre><h3><strong>Our Python Script: An End-to-End Walkthrough</strong></h3><p><strong>Note</strong>: <em>This is a </em><strong><em>single</em></strong><em> script that I’ve created, but will be explaining the various pieces that make up the script in its entirety</em>.</p><p>The first part of the script involves importing the required libraries and configuring logging to track our progress. It’ll be a bit difficult to monitor the execution of our script without logging of some sort:</p><pre>import boto3 #type:ignore<br>import os<br>import sys<br>import logging<br>from botocore.exceptions import ClientError # type:ignore<br>import time<br><br># Configure your logging<br>logging.basicConfig(level=logging.INFO, format=&quot;%(asctime)s %(levelname)s: %(message)s&quot;)</pre><p>As part of our deployment, we want to ensure that we have an encrypted EBS volume that’ll be attached to the EC2 instance, and so we’ll need to verify the existence of a KMS Key OR create a net-new key. A KMS client is created using the boto3 library, with the AWS region taken from the environment variable AWS REGION. The function will call kms_client.list_aliases() to retrieve a list of all existing aliases in the AWS account, and will then iterate over the aliases to check if the desired alias (alias/whatever-your-alias-name-should-be) already exists.</p><pre>def create_kms_key():<br>    &quot;&quot;&quot;Create or retrieve a KMS key for EBS encryption.&quot;&quot;&quot;<br>    kms_client = boto3.client(&quot;kms&quot;, region_name=os.getenv(&quot;AWS_REGION&quot;))<br>    alias_name = &quot;alias/whatever-your-alias-name-should-be&quot;<br><br>    try:<br>        response = kms_client.list_aliases()<br>        for alias in response.get(&quot;Aliases&quot;, []):<br>            if alias[&quot;AliasName&quot;] == alias_name:<br>                logging.info(f&quot;KMS key already exists: {alias_name} ({alias[&#39;TargetKeyId&#39;]})&quot;)<br>                return alias[&quot;TargetKeyId&quot;]<br><br>        response = kms_client.create_key(Description=&quot;AMI Encryption Key&quot;)<br>        key_id = response[&quot;KeyMetadata&quot;][&quot;KeyId&quot;]<br>        kms_client.create_alias(AliasName=alias_name, TargetKeyId=key_id)<br>        logging.info(f&quot;KMS key created: {alias_name} ({key_id})&quot;)<br>        return key_id<br>    except ClientError as e:<br>        logging.error(f&quot;Error managing KMS key: {e}&quot;)<br>        sys.exit(1)</pre><p>If there’s any desire to securely access the EC2 instance in the future for additional testing and validation, another enhancement for our script is to add in the creation of a key pair. Though not entirely required, this may be necessary if you’d like to access the instances locally:</p><pre>def create_key_pair():<br>    &quot;&quot;&quot;Create a key pair for SSH access.&quot;&quot;&quot;<br>    ec2_client = boto3.client(&quot;ec2&quot;, region_name=os.getenv(&quot;AWS_REGION&quot;))<br>    key_pair_name = os.getenv(&quot;KEY_PAIR_NAME&quot;)<br><br>    try:<br>        ec2_client.create_key_pair(KeyName=key_pair_name)<br>        logging.info(f&quot;Key pair {key_pair_name} created.&quot;)<br>    except ClientError as e:<br>        if &quot;InvalidKeyPair.Duplicate&quot; in str(e):<br>            logging.info(f&quot;Key pair {key_pair_name} already exists.&quot;)<br>        else:<br>            logging.error(f&quot;Error creating key pair: {e}&quot;)<br>            sys.exit(1)</pre><p>I’m a security person by nature, and so this wouldn’t be a robust python script without some added security layers. In this portion of the script, we are simply wanting to restrict access to our personal IP. Now, I know what you’re thinking, which is “<em>why would you restrict access to </em><strong><em>only</em></strong><em> your IP if you may end up sharing the AMI with another user or account?”.</em> The answer is simple — You can always UPDATE the security group rules AND this script, so no need to panic 🙂!</p><pre>def create_security_group():<br>    &quot;&quot;&quot;Create a security group with SSH access restricted to your IP.&quot;&quot;&quot;<br>    ec2_client = boto3.client(&quot;ec2&quot;, region_name=os.getenv(&quot;AWS_REGION&quot;))<br>    security_group_name = os.getenv(&quot;SECURITY_GROUP_NAME&quot;)<br>    my_public_ip = os.getenv(&quot;MY_PUBLIC_IP&quot;)<br><br>    try:<br>        response = ec2_client.create_security_group(<br>            Description=&quot;AMI Security Group&quot;, GroupName=security_group_name<br>        )<br>        sg_id = response[&quot;GroupId&quot;]<br>        ec2_client.authorize_security_group_ingress(<br>            GroupId=sg_id,<br>            IpPermissions=[<br>                {<br>                    &quot;IpProtocol&quot;: &quot;tcp&quot;,<br>                    &quot;FromPort&quot;: 22,<br>                    &quot;ToPort&quot;: 22,<br>                    &quot;IpRanges&quot;: [{&quot;CidrIp&quot;: f&quot;{my_public_ip}/32&quot;}],<br>                }<br>            ],<br>        )<br>        logging.info(f&quot;Security group {security_group_name} created with ID: {sg_id}&quot;)<br>        return sg_id<br>    except ClientError as e:<br>        logging.error(f&quot;Error managing security group: {e}&quot;)<br>        sys.exit(1)</pre><p>Who say’s that we can’t have a little fun and launch a secure, encrypted EC2 instance with encrypted EBS volumes as part of this deployment? This portion of the script does just that, and also executes the following:</p><ul><li>Uses environment variables to define key instance properties (AMI ID, instance type, key pair name).</li><li>Attaches an encrypted EBS volume as the root device, using a specified KMS key for encryption.</li><li>Associates the instance with a security group (identified by its ID).</li><li>Tags the instance with a name (“SecureInstance”).</li><li>Waits for the instance to reach the “running” state before completing the process.</li></ul><pre>def launch_instance(sg_id, kms_key_id):<br>    &quot;&quot;&quot;Launch an encrypted EC2 instance.&quot;&quot;&quot;<br>    ec2_client = boto3.client(&quot;ec2&quot;, region_name=os.getenv(&quot;AWS_REGION&quot;))<br>    try:<br>        response = ec2_client.run_instances(<br>            ImageId=os.getenv(&quot;BASE_AMI_ID&quot;),<br>            InstanceType=os.getenv(&quot;INSTANCE_TYPE&quot;),<br>            KeyName=os.getenv(&quot;KEY_PAIR_NAME&quot;),<br>            SecurityGroupIds=[sg_id],<br>            MinCount=1,<br>            MaxCount=1,<br>            BlockDeviceMappings=[<br>                {<br>                    &quot;DeviceName&quot;: &quot;/dev/xvda&quot;,<br>                    &quot;Ebs&quot;: {&quot;VolumeSize&quot;: 10, &quot;VolumeType&quot;: &quot;gp3&quot;, &quot;Encrypted&quot;: True, &quot;KmsKeyId&quot;: kms_key_id},<br>                }<br>            ],<br>            TagSpecifications=[<br>                {<br>                    &quot;ResourceType&quot;: &quot;instance&quot;,<br>                    &quot;Tags&quot;: [{&quot;Key&quot;: &quot;Name&quot;, &quot;Value&quot;: &quot;SecureInstance&quot;}],<br>                }<br>            ],<br>        )<br>        instance_id = response[&quot;Instances&quot;][0][&quot;InstanceId&quot;]<br>        logging.info(f&quot;Instance {instance_id} launched.&quot;)<br>        wait_for_instance_state(instance_id, &quot;running&quot;)<br>        return instance_id<br>    except ClientError as e:<br>        logging.error(f&quot;Error launching EC2 instance: {e}&quot;)<br>        sys.exit(1)</pre><p>The next part of our script is probably what I’m the most excited about — Creating our OWN custom AMI! Okay, so this little snippet in our code is deigned to generate a reusable and secure AMI based on the state of a running EC2 instance. It will use the EC2 create_image API to initiate the creation of an AMI, provides options to customize the AMI’s name, description, and behavior during the creation process (e.g., avoiding instance reboot), and returns the AMI ID for further use or tracking:</p><pre>def create_ami(instance_id):<br>    &quot;&quot;&quot;Create an AMI from the instance.&quot;&quot;&quot;<br>    ec2_client = boto3.client(&quot;ec2&quot;, region_name=os.getenv(&quot;AWS_REGION&quot;))<br>    try:<br>        ami_name = &quot;SecureAMI&quot;<br>        response = ec2_client.create_image(<br>            InstanceId=instance_id,<br>            Name=ami_name,<br>            Description=&quot;A secure AMI created from SecureInstance&quot;,<br>            NoReboot=True,<br>        )<br>        ami_id = response[&quot;ImageId&quot;]<br>        logging.info(f&quot;AMI creation initiated: {ami_id}&quot;)<br>        return ami_id<br>    except ClientError as e:<br>        logging.error(f&quot;Error creating AMI: {e}&quot;)<br>        sys.exit(1)</pre><p>We’ll include a verification step after the AMI has been created to confirm that the AMI is in fact “owned” by our AWS account:</p><pre>def verify_ami_ownership(ami_id):<br>    &quot;&quot;&quot;Verify that the created AMI is owned by your AWS account.&quot;&quot;&quot;<br>    ec2_client = boto3.client(&quot;ec2&quot;, region_name=os.getenv(&quot;AWS_REGION&quot;))<br>    try:<br>        response = ec2_client.describe_images(ImageIds=[ami_id])<br>        owner_id = response[&quot;Images&quot;][0][&quot;OwnerId&quot;]<br>        logging.info(f&quot;AMI {ami_id} is owned by account: {owner_id}&quot;)<br>    except ClientError as e:<br>        logging.error(f&quot;Error verifying AMI ownership: {e}&quot;)<br>        sys.exit(1)</pre><p>And finally, we’ll close out the script using the main function, with a focus on the following:</p><ul><li>Validating the required environment variables for the process.</li><li>Calling a series of functions to handle encryption, security, EC2 instance launching, and AMI creation.</li><li>Logging the progress and ensuring errors are handled appropriately.</li></ul><pre>def main():<br>    # Validate required environment variables<br>    required_vars = [<br>        &quot;AWS_REGION&quot;,<br>        &quot;AWS_ACCESS_KEY_ID&quot;,<br>        &quot;AWS_SECRET_ACCESS_KEY&quot;,<br>        &quot;INSTANCE_TYPE&quot;,<br>        &quot;BASE_AMI_ID&quot;,<br>        &quot;KEY_PAIR_NAME&quot;,<br>        &quot;SECURITY_GROUP_NAME&quot;,<br>        &quot;MY_PUBLIC_IP&quot;,<br>    ]<br>    missing_vars = [var for var in required_vars if not os.getenv(var)]<br>    if missing_vars:<br>        logging.error(f&quot;Missing required environment variables: {&#39;, &#39;.join(missing_vars)}&quot;)<br>        sys.exit(1)<br><br>    # Run the secure AMI creation process<br>    logging.info(&quot;Starting secure AMI creation process...&quot;)<br>    kms_key_id = create_kms_key()<br>    create_key_pair()<br>    sg_id = create_security_group()<br>    instance_id = launch_instance(sg_id, kms_key_id)<br>    ami_id = create_ami(instance_id)<br>    verify_ami_ownership(ami_id)<br>    logging.info(&quot;Secure AMI creation process completed successfully.&quot;)<br><br><br>if __name__ == &quot;__main__&quot;:<br>    main()</pre><h3><strong>Automating Our Script Using A GitHub Actions Workflow</strong></h3><p>Now, you can’t work with the AWS Boto3 SDK without throwing in some exciting pipeline action. You must be sure that you <strong>DO NOT hardcode</strong> your AWS Account ID, AWS Access Key &amp; Secret Access Key, and <em>instead</em>, add these as GitHub Secrets. Here are a few things of note regarding this workflow, just in case you’ve never used GitHub Actions, starting with the type of workflow that’s created:</p><h4>Trigger</h4><ul><li><strong>workflow_dispatch:</strong><br>The pipeline is manually triggered and accepts several <strong>inputs</strong> related to the configuration of the AWS resources and environment. What this means is that you would navigate to your GitHub repo, head to the ‘Actions’ tab, find the workflow, and add the values you’d like as inputs before running it. You can run the workflow as MANY times as you’d like 😄. These are the input options for the workflow:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/664/1*1L5PhBKtzoYkjdfFVOzYKg.png" /></figure><h4>Job</h4><ul><li><strong>create-ami:</strong><br>This job runs the secure AMI creation process in a series of steps on an ubuntu-latest runner.</li></ul><h4>Secure Ami Creation Script Step</h4><ul><li>Executes the Python script (secure_ami.py) to perform the secure AMI creation steps.</li></ul><h4>Wait for the AMI Availability Step</h4><ul><li>Ensures the created AMI is in the available state before proceeding.</li></ul><h4>Verify AMI Sharing Step</h4><ul><li>Checks to see if the newly created AMI has been shared with other AWS accounts.</li></ul><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/9d41dbf20fdf3bb0f093e2f304910e06/href">https://medium.com/media/9d41dbf20fdf3bb0f093e2f304910e06/href</a></iframe><h3>Making The Magic Happen</h3><p>There are a few different approaches that you can take to create the desired resources. If you’re interested in performing a simple dry run locally via Python, you’d execute the following command below, and also run the export of the variables that was mentioned earlier:</p><p>DRY_RUN=true python secure_ami.py</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*V5gMfP66Lp8cLeAzPp7WRw.png" /></figure><p>Alternatively, if you’d like to skip the dry run and see the magic unfold via the pipeline instead, you can <strong>kick it off locally</strong>, but you must ensure that you’ve pushed your configuration files to the desired repo (<em>pushing the files</em> <em>will not trigger the workflow since we’re using a workflow dispatch</em>):</p><p>gh workflow list</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*r-8MuU2N7vZx9UTkp0EPYw.png" /></figure><pre>gh workflow run secure_ami.yml \ <br>-- field region=&quot;your-region&quot; \ <br>-- field instance_type=&quot;t2.micro&quot; \ <br>-- field base_ami_id=&quot;ami-xxxxx&quot; \<br>-- field key_pair=&quot;key-pair-name&quot; \ <br>-- field security_group=&quot;sg-name&quot; \ <br>-- field my_ip=&quot;x.x.x.x&quot;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*p_HGTxLmPeE1KYDn3tqWdA.png" /></figure><h3>Validating Our Deployment</h3><p>With a quick glance at our Pipeline, we can see that we have a successful deployment 🙌:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nJxVeYFnu4wbhdy0NVxQmQ.png" /><figcaption>Screenshot of the GHA Workflow Output — Shows A Successful Execution</figcaption></figure><p>If we navigate to our AWS Console, we’ll only validate a few things, just to serve as proof that resources were actually created:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*l_-doXGLZsGIaWDs6bLooA.png" /><figcaption>Custom, Secure AMI</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KoDmF9BAWt3SJemtfDU-Vw.png" /><figcaption>Encrypted Volume</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3zFfv0aAUCF7b5RSF-65hg.png" /><figcaption>Secure EC2 Instance</figcaption></figure><h3>Conclusion</h3><p>And there you have it! 🚀 You’ve now set up a streamlined, secure, and automated pipeline to create encrypted AMIs and launch EC2 instances like a pro. 🔒 By leveraging GitHub Actions, Python, and AWS, you’ve taken a major step toward simplifying infrastructure management while following best practices for security. 🌟</p><p>Ready to take this to the next level? Start customizing and scaling this workflow to fit your unique cloud needs. The sky’s the limit! ☁️</p><p>Happy coding! 🎉</p><h3>Thank you for being a part of the community</h3><p><em>Before you go:</em></p><ul><li>Be sure to <strong>clap</strong> and <strong>follow</strong> the writer ️👏<strong>️️</strong></li><li>Follow us: <a href="https://x.com/inPlainEngHQ"><strong>X</strong></a> | <a href="https://www.linkedin.com/company/inplainenglish/"><strong>LinkedIn</strong></a> | <a href="https://www.youtube.com/channel/UCtipWUghju290NWcn8jhyAw"><strong>YouTube</strong></a> | <a href="https://newsletter.plainenglish.io/"><strong>Newsletter</strong></a> | <a href="https://open.spotify.com/show/7qxylRWKhvZwMz2WuEoua0"><strong>Podcast</strong></a></li><li><a href="https://cofeed.app/"><strong>Check out CoFeed, the smart way to stay up-to-date with the latest in tech</strong></a> <strong>🧪</strong></li><li><a href="https://differ.blog/"><strong>Start your own free AI-powered blog on Differ</strong></a> 🚀</li><li><a href="https://discord.gg/in-plain-english-709094664682340443"><strong>Join our content creators community on Discord</strong></a> 🧑🏻‍💻</li><li>For more content, visit <a href="https://plainenglish.io/"><strong>plainenglish.io</strong></a> + <a href="https://stackademic.com/"><strong>stackademic.com</strong></a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=22dc721bd985" width="1" height="1" alt=""><hr><p><a href="https://python.plainenglish.io/creating-a-secure-encrypted-ami-using-boto3-python-and-github-actions-22dc721bd985">Creating a Secure, Encrypted AMI Using Boto3, Python, and GitHub Actions</a> was originally published in <a href="https://python.plainenglish.io/">Python in Plain English</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Deploy a Dockerized Flask Application to AWS ECS using CircleCI and Terraform]]></title>
            <link>https://aws.plainenglish.io/deploy-a-dockerized-flask-application-to-aws-ecs-using-circleci-and-terraform-e2216284e7a3?source=rss-6e1dbb973ea1------2</link>
            <guid isPermaLink="false">https://medium.com/p/e2216284e7a3</guid>
            <category><![CDATA[pipeline-automation]]></category>
            <category><![CDATA[aws-ecs]]></category>
            <category><![CDATA[aws-kms]]></category>
            <category><![CDATA[aws-fargate]]></category>
            <category><![CDATA[circleci]]></category>
            <dc:creator><![CDATA[Katoria Henry]]></dc:creator>
            <pubDate>Thu, 10 Oct 2024 11:04:38 GMT</pubDate>
            <atom:updated>2024-10-10T13:50:29.784Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oFR3S0JkadQWNVcfCBAhnA.gif" /><figcaption>Simple Flow Chart</figcaption></figure><p>In today’s software development landscape, <strong>automating deployments</strong> and managing infrastructure as code are essential skills for developers and DevOps engineers. This tutorial will walk you through setting up a <strong>CI/CD (Continuous Integration/Continuous Deployment) pipeline</strong> to automate the process of building and deploying a simple web application. We’ll be using <strong>Docker</strong> to containerize a basic <strong>Flask</strong> application (a lightweight web framework for Python), <strong>CircleCI</strong> to automate the building and testing of our code, and <strong>Terraform</strong> to manage the infrastructure setup on <strong>AWS (Amazon Web Services)</strong>.</p><p>Our goal is to deploy the Flask application to <strong>ECS (Elastic Container Service)</strong>, a managed container orchestration service provided by AWS that makes it easy to run, stop, and manage containers. By the end of this guide, you’ll understand how these tools work together to create a fully automated, scalable, and resilient deployment pipeline. Whether you’re a beginner or have some experience with cloud services, this tutorial will help you get hands-on experience with modern DevOps practices.</p><h3>Prerequisites</h3><ol><li><strong>AWS Account</strong> with permissions to manage ECS, ECR, IAM, and related services.</li><li><strong>CircleCI account</strong>, linked to your GitHub or Bitbucket repository.</li><li><strong>Terraform installed</strong> locally.</li><li><strong>Docker installed</strong> locally.</li><li><strong>Flask</strong> installed locally.</li></ol><p><strong>Step 1: Create a Simple Flask Application</strong></p><p>We’re going to kick things off by getting our directory structure properly configured as we want to ensure that we have ALL of the necessary files. Below, you’ll find the files that I’ve used for this deployment (your deployment files should be similar):</p><pre>.<br>├── .circleci/<br>│   └── config.yml<br>├── python/<br>│   ├── app.py<br>│   └── .env<br>├── terraform/<br>│   ├── ecs.tf<br>│   ├── main.tf<br>│   └── variables.tf<br>├── Dockerfile<br>├── requirements.txt<br>├── .gitignore<br>└── README.md</pre><p>We’ll start by adding our Python files first. Feel free to use whatever code you’d prefer for app.py, but here’s a simple snippet of what that may consist of. <strong>Note</strong>: You may want to ensure that Flask is <strong>installed</strong> locally or within your virtual environment, or you will most likely see a ‘problem’ indicated in your IDE, such as what we have below:</p><p>app.py</p><pre>from flask import Flask<br><br>app = Flask(__name__)<br><br>@app.route(&#39;/&#39;)<br>def hello_world():<br>    return &#39;Hello, CircleCI and ECS!&#39;<br><br>if __name__ == &#39;__main__&#39;:<br>    app.run(host=&#39;0.0.0.0&#39;, port=5001)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/695/1*16lSQLIEIIjUFG1T4OOQHA.png" /><figcaption>Simply install Flask if it is not installed if you see this ‘problem’</figcaption></figure><p>It’s important to note that whenever you’re working with “sensitive” values, you NEVER want to hard-code these config details directly within your code base. It’s a horrible security practice — flat out. Instead, you’ll want to use a file for your environment variables, such as the one that I have below (it is not required to use a <a href="https://docs.python.org/3/library/venv.html">Python env</a> for this deployment, though it is optional if you choose to do so):</p><p>.env</p><pre>AWS_REGION=your-aws-region<br>AWS_ACCOUNT_ID=your-aws-account-id<br>ECR_REPOSITORY_NAME=your-ecr-repo-name<br>IMAGE_TAG=your-image-tag<br>KMS_KEY_ALIAS=your-key-alias</pre><p>With any Python application, it is normal to also have a requirements file as well as this helps with dependency management, though if your project is very small, you probably would not need this file. Nevertheless, below you’ll find our super-duper simple requirements file:</p><p>requirements.txt</p><pre>Flask==3.0.3</pre><p>If you’d like, you can run a <em>quick</em> test against the simple app locally by executing the file, and visiting the webpage in the browser using <a href="http://localhost:5001/">http://localhost:</a>&lt;port&gt;:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/878/1*VBrIrcLV2b8m0jsC3Nc1yw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YRfB54_0vz8WVCg_1qjppQ.png" /></figure><p><strong>Step 2: Dockerize the Flask Application</strong></p><p>The <strong>Dockerfile</strong> is a crucial part of this deployment because it defines how the application is containerized. In the context of deploying a Flask application to AWS ECS using CircleCI and Terraform, here’s why the Dockerfile is important:</p><ul><li>The Dockerfile specifies the environment in which the Flask application will run. It starts with a <strong>base image</strong>, such as python:3.9-slim, and installs all the necessary dependencies (like Flask) for the application to work. This ensures consistency across different environments (local, testing, staging, and production), as the container will always run the same code in the same environment.</li><li>It handles the installation of <strong>application dependencies</strong>, such as the Python packages listed in the requirements.txt file. This allows you to package everything needed for the application to run within the container, avoiding the problem of &quot;it works on my machine&quot; but fails elsewhere.</li><li>Since Docker containers can run on any platform that supports Docker, a Dockerfile makes your application <strong>portable</strong> and <strong>reproducible</strong>. It ensures that the application behaves the same way regardless of where it is deployed, whether on a developer’s laptop, a CI/CD server (like CircleCI), or a cloud environment (like AWS ECS).</li><li>By keeping the Dockerfile in version control along with the application code, changes to the deployment environment (like updates to dependencies or base images) are tracked. This helps maintain a history of changes and allows you to <strong>roll back</strong> to previous configurations if needed.</li><li>In our <strong>CI/CD pipeline</strong>, the Dockerfile serves as the <strong>blueprint</strong> for building the Docker image. CircleCI uses the Dockerfile to create an image of the Flask application, which is then pushed to a container registry (ECR). The same image is then pulled by the ECS service for deployment. This automation speeds up development workflows and minimizes manual configuration.</li></ul><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/e36968d90293e28a879983dfde7d0c6d/href">https://medium.com/media/e36968d90293e28a879983dfde7d0c6d/href</a></iframe><p><strong>Step 3: Use Terraform to Set Up AWS Infrastructure</strong></p><p>In this step, we’re going to use Terraform to create the necessary AWS resources: an ECS cluster, ECR repository, IAM roles, and networking configurations. Remember, you SHOULD NOT be hard-coding any of your environment variables within the TF configuration directly. It’s important to also be mindful that since we will be creating an ECR repo, KMS Key, and ECS Cluster &amp; Service, you MUST ensure that you have the <strong>appropriate permissions</strong> to create these resources or you will not be able to apply the Terraform configuration locally. We’ll start by creating our <strong>main.tf </strong>file:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/4fd051b051d121b8a8441f6d9d4f6839/href">https://medium.com/media/4fd051b051d121b8a8441f6d9d4f6839/href</a></iframe><p>Next, we’ll dive into our <strong>ecs.tf</strong> config file. Remember, for any of the resources that you are wanting to create for ECS, please note that if you have previously used ECR images in your task definition, you <em>may</em> have an existing execution IAM role. Your Terraform configuration <strong>will not </strong>be applied successfully if there is an existing role in place. You have the option of using the existing role (if you have one), or creating a net-new role, such as what I have done below:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/5be279606d049d9e65c2926dda9fecb9/href">https://medium.com/media/5be279606d049d9e65c2926dda9fecb9/href</a></iframe><p>And finally, we’ll wrap things up with our last file for our terraform variables….NO, this is not the same file that we created earlier for Python. This is a totally separate, and different configuration type for Terraform specifically:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/7aa0f2054ade7c5c68853708f984e92e/href">https://medium.com/media/7aa0f2054ade7c5c68853708f984e92e/href</a></iframe><p>So, what are <strong><em>some</em></strong> of the key benefits of using Terraform for our deployment?</p><ul><li>It enables the <strong>automatic creation, updating, and deletion of infrastructure</strong>, making deployments more efficient and reliable.</li><li>If something goes wrong, you can <strong>roll back changes</strong> to a previous version, just like you would with application code.</li><li>You can easily create and tear down test environments, which is especially useful for <strong>continuous integration/continuous deployment (CI/CD)</strong> workflows.</li><li>In this deployment, you can use CircleCI to trigger Terraform to provision resources or update configurations whenever changes are made to the infrastructure code. This results in a <strong>fully automated pipeline</strong> where changes to code and infrastructure are deployed together.</li></ul><p>Alright, now I’m sure that by this point you’re anticipating some action, so let’s jump into the Terraform magic, and run the following commands. You should have <em>at least </em>eight (8) resources provisioned successfully:</p><pre>terraform init<br>terraform plan<br>terraform apply</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TKDGQuAF2ihCPrC89PYVUg.png" /><figcaption>Terraform Plan Results</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/973/1*6t9zLww30ea7NXzPzD0XNw.png" /><figcaption>Terraform Apply Results</figcaption></figure><p><strong>Step 4: Configure CircleCI for your CI/CD Pipeline</strong></p><p>With our AWS resources now deployed, and our sample Python application tested locally for verification purposes, we must now ensure that we configure a pipeline to manage our deployment. You’ll need to create a <strong>.circleci</strong> directory, and create a <strong>config.yml</strong> file within this directory. Without this file, your pipeline will NOT run, and you’ll miss out on all of the fun that’s to come!</p><p>Be sure that you have pushed ALL of your files to your designated repo at this time (the config.yml file will be needed to trigger the pipeline from your repo directly). This ‘push’ will <em>actually</em> trigger your workflow directly (<em>which may fail initially</em>), but you’ll have the ability to update the settings in a later step and re-trigger the workflow for a <strong>successful</strong> run:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/1e79fec8a0620c063b24aed5fa188726/href">https://medium.com/media/1e79fec8a0620c063b24aed5fa188726/href</a></iframe><p>Let’s break this config.yml file down a bit to understand <strong>exactly</strong> what we’re doing here:</p><p><strong>Defines a Docker-based executor (</strong><strong>docker-executor)</strong>:</p><ul><li>Uses a Docker image (circleci/python:3.9) to run the jobs, providing a Python environment where the commands will execute.</li></ul><p><strong>Build Job</strong>:</p><ul><li><strong>Checks out the code</strong> from the repository.</li><li><strong>Sets up a remote Docker environment</strong> to enable building Docker images within the CircleCI environment.</li><li><strong>Installs the AWS CLI</strong> to allow interaction with AWS services.</li><li><strong>Logs in to Amazon ECR</strong> using the AWS CLI, enabling the pushing of Docker images to the registry.</li><li><strong>Builds a Docker image</strong> for the Flask application using the provided Dockerfile.</li><li><strong>Tags the Docker image</strong> with the repository and image tag.</li><li><strong>Pushes the Docker image to ECR</strong>, making it available for deployment.</li></ul><p><strong>Deploy Job</strong>:</p><ul><li><strong>Installs the AWS CLI</strong> to perform AWS-specific operations.</li><li><strong>Updates the ECS service</strong> to use the newly pushed Docker image, forcing a new deployment to pull the latest version of the image from ECR.</li></ul><p><strong>Workflows Section</strong>:</p><ul><li>Defines a <strong>workflow named </strong><strong>build_and_deploy</strong> that runs both the build and deploy jobs.</li><li>Ensures that the <strong>deploy job only runs after the </strong><strong>build job completes successfully</strong>, creating a sequential pipeline.</li></ul><p><strong>Step 5: Updating the Circle CI Settings &amp; Executing the Pipeline</strong></p><p>With all of our code now pushed to our desired repo, we must ensure that we get things configured within the Circle CI console correctly. The <strong><em>assumption</em></strong> is that you have signed up for an account, and that you have integrated your desired GitHub/Bitbucket repo accordingly.</p><p>With your repo integrated, you must first ensure that you have setup your project, following the guidance in the console:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gkNpQLpXJiZE3nKg38W5aw.png" /></figure><p>Be sure that you select the existing config file that you just pushed, or if you dare to be dangerous, create a net-new config.yml file:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/944/1*fFk_Ps598GhbImnIAWnaMQ.png" /></figure><p>**Once you’ve selected the correct config.yml file, DO NOT panic as you may see a pipeline trigger**</p><p>Navigate to &gt; Project Settings &gt; Environment Variables &gt; Enter your environment variables that you created earlier, along with the AWS Access/Secret Access Key:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*EEGw95vpSOYaFrds1TlVTw.png" /><figcaption>Environment Variables</figcaption></figure><p>After you’ve successfully added all required env variables, navigate back to &gt; Projects &gt; Select Your Project &gt; Trigger Pipeline:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cBjv5DLaQQYrnPf-PT4_NA.png" /></figure><p>From here, you can sit back and watch the the various phases of the pipeline execute (super neat!!), all of which should be successful if you have the CORRECT environment variables in place. We’ll start by showing some of the <strong>Build Job</strong> run details:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bF91ddU0S5nDTR8axn3MBg.png" /><figcaption>Build Job</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aAs75r0ccI3GmrU6Hen2Pg.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tlamfePw-GFmTYQXQEZxmg.png" /></figure><p>Next, let’s check out what was done for our <strong>Deploy Job</strong>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VpE8txCYx2AXV7HDrKxJTA.png" /><figcaption>Deploy Job</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*STYFRe67FnZXZfnDYEwC3A.png" /><figcaption>ECS Service Updated</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8Qim_7DQaUGCW2Onnx99wQ.png" /><figcaption>End-to-End Success!</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HXHjSvgg7MdnaQPkq0toZw.png" /><figcaption>GitHub Repo Pipeline Confirmation</figcaption></figure><p><strong>Wrapping Things Up</strong></p><p>I know you may be wondering if in fact we actually have AWS resources that were created via Terraform, and whether or not the pipeline actually updated the ECS Service…well, if you’ve followed along with my tutorial and your configuration is <em>similar </em>to the example code I’ve provided, you will see that all of the resources were in fact created successfully, and we can verify that we actually have an active cluster, container, active deployment, and so forth:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fUl13rxp_kjeohB_s7gubQ.png" /><figcaption>Flask ECS Cluster</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nta1RB4hHOV46KiSFkcCDw.png" /><figcaption>Flask Container</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ajp8EDsWNENsgwLze4oN2w.png" /><figcaption>ECR Repo</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_lkEAGbbVH1eV9_IxZe8sw.png" /><figcaption>Flask Service</figcaption></figure><p>Dance Break: Did you all see that it ONLY took 48 seconds for this pipeline to run just ‘two’ workflows, end-to-end ? Now do you see why pipelines are everythiinnnngggg??!! Phew, it doesn’t get any better than that! Oh, and don’t forget to remove all AWS resources via <strong>terraform destroy!</strong></p><p>Thanks for stopping by for the read! Follow me on LinkedIn if you’d like to see more interesting content like this! <a href="https://www.linkedin.com/in/katoria-henry-2018/">https://www.linkedin.com/in/katoria-henry-2018/</a></p><h3>In Plain English 🚀</h3><p><em>Thank you for being a part of the </em><a href="https://plainenglish.io/"><strong><em>In Plain English</em></strong></a><em> community! Before you go:</em></p><ul><li>Be sure to <strong>clap</strong> and <strong>follow</strong> the writer ️👏<strong>️️</strong></li><li>Follow us: <a href="https://twitter.com/inPlainEngHQ"><strong>X</strong></a> | <a href="https://www.linkedin.com/company/inplainenglish/"><strong>LinkedIn</strong></a> | <a href="https://www.youtube.com/channel/UCtipWUghju290NWcn8jhyAw"><strong>YouTube</strong></a> | <a href="https://discord.gg/in-plain-english-709094664682340443"><strong>Discord</strong></a> | <a href="https://newsletter.plainenglish.io/"><strong>Newsletter</strong></a></li><li>Visit our other platforms: <a href="https://cofeed.app/"><strong>CoFeed</strong></a> | <a href="https://differ.blog/"><strong>Differ</strong></a></li><li>More content at <a href="https://plainenglish.io/"><strong>PlainEnglish.io</strong></a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e2216284e7a3" width="1" height="1" alt=""><hr><p><a href="https://aws.plainenglish.io/deploy-a-dockerized-flask-application-to-aws-ecs-using-circleci-and-terraform-e2216284e7a3">Deploy a Dockerized Flask Application to AWS ECS using CircleCI and Terraform</a> was originally published in <a href="https://aws.plainenglish.io/">AWS in Plain English</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[AWS EKS: Observability with Istio, Grafana & Prometheus]]></title>
            <link>https://levelup.gitconnected.com/aws-eks-observability-with-istio-grafana-prometheus-9e6683325964?source=rss-6e1dbb973ea1------2</link>
            <guid isPermaLink="false">https://medium.com/p/9e6683325964</guid>
            <dc:creator><![CDATA[Katoria Henry]]></dc:creator>
            <pubDate>Tue, 30 Apr 2024 22:11:49 GMT</pubDate>
            <atom:updated>2024-04-30T23:52:23.290Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8Si-cZdGBEbd2_nSyl9fTw.png" /></figure><p>Hello world! We are back again and will be diving into configuring &amp; using the Istio Service Mesh with an EKS cluster. We’ll visualize our deployment via Grafana, and tack on a special Prometheus data source as well for further monitoring. If you’re fairly new to Istio, Granfa, or Prometheus, check out the brief overview below:</p><p><strong>Istio</strong> is an open-source service mesh platform that provides a uniform way to connect, manage, and secure microservices. It offers features like traffic management, observability, policy enforcement, and security. With Istio, you can gain insights into the interactions between services, control traffic flow, enforce security policies, and more, all without modifying your application code.</p><p><strong>Grafana</strong> is an open-source analytics and monitoring platform that allows you to query, visualize, and understand metrics from various data sources. It provides features like customizable dashboards, alerting, and powerful querying capabilities. Grafana is commonly used for visualizing metrics collected by Prometheus and other monitoring systems.</p><p><strong>Prometheus</strong> is an open-source monitoring and alerting toolkit designed for reliability and scalability. It collects metrics from monitored targets by scraping HTTP endpoints at regular intervals, evaluates rule expressions, and triggers alerts when specified conditions are met. Prometheus is highly customizable and integrates seamlessly with Kubernetes for monitoring containerized workloads.</p><p>Now that we have those details out of the way, let’s get things started by identifying the necessary prerequisites for the tutorial.</p><p>**Keep in mind that this is a *demo* setup and the configuration for a production environment will require more stringent requirements!</p><p><strong>Pre-Reqs</strong></p><ul><li>An <a href="https://aws.amazon.com/free/?all-free-tier.sort-by=item.additionalFields.SortRank&amp;all-free-tier.sort-order=asc&amp;awsf.Free%20Tier%20Types=*all&amp;awsf.Free%20Tier%20Categories=*all">AWS Account</a></li><li>IAM Roles Configured — Roles for the Cluster &amp; Nodes</li><li><a href="https://docs.aws.amazon.com/emr/latest/EMR-on-EKS-DevelopmentGuide/setting-up-eksctl.html">eksctl </a>&amp; <a href="https://docs.aws.amazon.com/eks/latest/userguide/install-kubectl.html">kubectl</a> installed</li><li>IDE of your choice</li></ul><p><strong>Step 1</strong></p><p>We’ll start by assuming that you’ll need to create an EKS cluster from scratch. Before creating your cluster, I would highly recommend that you have custom roles identified, <strong><em>such as a custom EKS role, and at least a service role for the cluster and node groups</em></strong>. Without the necessary permissions, you will not be able to create nor interact with your cluster.</p><ul><li><strong>Note </strong>that it will take ~15–20 minutes to create your demo cluster. There are a few different approaches in configuring your EKS cluster &amp; Node Groups, such as the <strong><em>options</em></strong> below (choose the command(s) that work best for you):</li></ul><pre>eksctl create cluster --name &lt;cluster name&gt; --region &lt;yourregion&gt; --nodes=4 --node-type=m5.large<br><br>eksctl create cluster --name &lt;cluster name&gt; --version &lt;clusterversion&gt; --region &lt;yourregion&gt; --nodegroup-name&lt;ngname&gt; --node-type=t2.micro --nodes-min 1 --nodes-max 2 --nodes 2<br><br>eksctl create cluster --name &lt;cluster name&gt; --region &lt;yourregion&gt; --nodegroup-name my-nodes --node-type t3.medium --nodes 3<br><br>aws eks create-cluster --region &lt;your region&gt; --name &lt;cluster name&gt; --kubernetes-version 1.28 - role-arn &lt;rolearn&gt; --resources-vpc-config subnetIds=subnet-<br>&lt;id&gt;,subnet-&lt;id&gt;,securityGroupIds=sg-&lt;id&gt;<br><br>aws eks create-nodegroup --region &lt;your region&gt; --cluster-name &lt;cluster name&gt; --nodegroup-name &lt;ngname&gt; --scaling-config minSize=2,maxSize=2,desiredSize=2 --subnets subnet-&lt;id&gt; subnet-&lt;id&gt; --ami-type AL2_x86_64 --node-role arn:aws:iam::&lt;accountid&gt;:role/&lt;name&gt;<br><br>eksctl create cluster --name &lt;cluster name&gt; --region &lt;yournregion&gt; --nodes=4 --node-type=m5.large<br><br>eksctl create cluster --name &lt;clustername&gt; --version &lt;clusterversion&gt; --region &lt;yourregion&gt; --nodegroup-name&lt;ngname&gt; --node-type=t2.micro --nodes-min 1 --nodes-max 2 --nodes 2<br><br>eksctl create cluster --name &lt;cluster name&gt; --region &lt;your region&gt; --nodegroup-name my-nodes --node-type t3.medium --nodes 3<br><br>aws eks create-cluster --region &lt;your region&gt; --name &lt;cluster name&gt; --kubernetes-version 1.28 --role-arn &lt;rolearn&gt; --resources-vpc-config subnetIds=subnet-&lt;id&gt;,subnet-&lt;id&gt;,securityGroupIds=sg-&lt;id&gt;<br><br>aws eks create-nodegroup --region &lt;your region&gt; --cluster-name &lt;cluster name&gt; - nodegroup-name &lt;ngname&gt; --scaling-config minSize=2,maxSize=2,desiredSize=2 --subnets subnet-&lt;id&gt; subnet-&lt;id&gt; --ami-type AL2_x86_64 --node-role arn:aws:iam::&lt;accountid&gt;:role/&lt;name&gt;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*hBs3fE-cIoYNiGwM" /></figure><ul><li>Let’s also create a net-new kubernetes namespace. This will be needed for the manifests we’ll create in a later step, and also helps with dividing cluster resources between users and projects. It is a best practice to create a new namespace whenever possible:</li></ul><pre>kubectl create namespace &lt;namespacename&gt;</pre><ul><li>Next, we’ll update the context that we want to use for our cluster, which requires modifying the kubeconfig file:</li></ul><pre>aws eks --region &lt;your region&gt; update-kubeconfig --name &lt;cluster name&gt;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*hUGtji0uAXymKNWM" /></figure><ul><li>With the cluster now created, let’s proceed by installing Istio locally, using the following command:</li></ul><pre>curl -L https://istio.io/downloadIstio | sh -<br>cd istio-*<br>export PATH=$PWD/bin:$PATH</pre><ul><li>To install Istio on EKS, let’s run the command below:</li></ul><pre>istioctl install --set profile=demo -y</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*YrDM075SVp8Gnzc8" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/714/0*5RQve5cmtk7QojmC" /><figcaption>**You’ll notice that this version of Istio doesn’t support the current version of our cluster, which may cause conflicts. To ensure we do not run into issues, we’ll need to update our cluster version (do this locally or via the console)**</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*R4E9RDCBMD4CZOpX" /></figure><ul><li>You’ll need to create a net-new nodegroup and remove the older group since we were required to update the Cluster. Verify that all nodes have reached a ‘ready’ state.</li></ul><pre>kubectl get nodes</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*O2r4oZI6xfzhbKnP" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*8aY8cewwCCbdkdGs" /></figure><ul><li>You <em>may</em> need to re-install istio, following the commands above. Once done, you should no longer see any warnings regarding the versioning conflicts:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*kEKluqhqynObRIw4" /></figure><ul><li>Finally, we’ll need to ensure that the pods have an Envoy sidecar injected so that Istio is able to manage the necessary service. Be sure that you use the correct namespace when running this command:</li></ul><pre>kubectl label namespace&lt;yournamespace&gt; istio-injection=enabled</pre><p><strong>Step 2</strong></p><p>We’re now going to jump into creating the first set of files that will be needed. We will keep things simple with this deployment and use a basic Nginx server with a custom HTML webpage for our applications.</p><ul><li>We’ll need to create a Dockerfile for the Nginx image we will be using along with a custom index.html file to replace the default index.html webpage. Following this creation, we’ll need to build our custom image:</li></ul><pre># Image of your choice (We&#39;re using Nginx)<br>FROM nginx:alpine<br><br># Copy your custom HTML file into the Nginx document root<br>COPY index.html /usr/share/nginx/html/index.html<br><br># Expose port 80<br>EXPOSE 80<br><br># Command to start Nginx<br>CMD [&quot;nginx&quot;, &quot;-g&quot;, &quot;daemon off;&quot;]</pre><pre>&lt;!DOCTYPE html&gt;<br>&lt;html lang=&quot;en&quot;&gt;<br>&lt;head&gt;<br>    &lt;meta charset=&quot;UTF-8&quot;&gt;<br>    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;<br>    &lt;title&gt;Katoria&#39;s DemoApps&lt;/title&gt;<br>    &lt;style&gt;<br>        body {<br>            background-color: black;<br>            color: white;<br>            font-family: Arial, sans-serif;<br>            text-align: center;<br>        }<br>        h1 {<br>            margin-top: 50px;<br>        }<br>        img {<br>            width: 400px;<br>            height: auto;<br>            margin-bottom: 20px;<br>        }<br>    &lt;/style&gt;<br>&lt;/head&gt;<br>&lt;body&gt;<br>    &lt;iframe src=&quot;https://giphy.com/embed/9Hx2Jhutoccy75DzIm&quot; width=&quot;480&quot; height=&quot;360&quot; frameBorder=&quot;0&quot; class=&quot;giphy-embed&quot; allowFullScreen&gt;&lt;/iframe&gt;&lt;p&gt;&lt;a href=&quot;https://giphy.com/gifs/clap-applaus-picard-9Hx2Jhutoccy75DzIm&quot;&gt;via GIPHY&lt;/a&gt;&lt;/p&gt;<br>    &lt;h1&gt;Katoria&#39;s DemoApps&lt;/h1&gt;<br>&lt;/body&gt;<br>&lt;/html&gt;</pre><p>Building the custom image:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*19altMAgolUJsf8A" /></figure><ul><li>Next, we’ll create our Istio Gateway &amp; Virtual Service as they will need to be added to our two deployments, both of which will leverage the custom image that we’ve built in the step above:</li></ul><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/84107fde35b75bf59672bb8a36e018c5/href">https://medium.com/media/84107fde35b75bf59672bb8a36e018c5/href</a></iframe><ul><li>Before applying your deployments, you <em>may</em> need to run a container from the image you’ve built using the <strong>docker run </strong>command if you are adding a customized webpage and/or depending on your custom deployments. Create at least TWO deployments using this sample deployment as a starting point:</li></ul><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/022174816f4859125eb991d6b6b46ce6/href">https://medium.com/media/022174816f4859125eb991d6b6b46ce6/href</a></iframe><ul><li>Next, let’s apply the deployments, istio gateway &amp; virtual service, using the following commands:</li></ul><pre>kubectl apply -f &lt;appname&gt;.yml<br>kubectl apply -f &lt;appname&gt;.yml<br>kubectl apply -f &lt;gatewayfile&gt;.yml</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/665/0*pqGsQw5qLjIeKcAL" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/720/0*V-Oqz38KrwV8bkN4" /></figure><ul><li>If you wanted to verify your deployments and/or pods, you can run the following command, using -n to specify a namespace. <strong>Note </strong>that it <em>may</em> take a few minutes to see the pods up and running. Re-run this command if you see that your pods are not ready, such as the below:</li></ul><pre>kubectl get pods -n &lt;namespace&gt;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*OB_-UOfynXR7kaUu" /><figcaption>Pods were still being spun up at this time</figcaption></figure><p><strong>Step 3</strong></p><p>Now that we have our deployments and necessary services and gateway up and running, we can proceed with the installation of Grafana, followed by the creation of Prometheus as a data source:</p><ul><li>To deploy Grafana using Helm, we will first start by creating a namespace:</li></ul><pre>kubectl create namespace &lt;name&gt;</pre><ul><li>Next, let’s get the Helm chart repo added as well:</li></ul><pre>helm repo add grafana https://grafana.github.io/helm-charts</pre><ul><li>And finally, we have to actually <em>install</em> Grafana via Helm:</li></ul><pre>helm install grafana grafana/grafana --namespace &lt;name&gt;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*sqcjsJl0PivX4Dnu" /></figure><ul><li>If you are following the <strong>exact </strong>steps that I have added above, you will notice that Grafana has a status of “Deployed”. However, you must <strong>also</strong> follow the instructions that are provided as part of this deployment, which include (1) generating your ‘admin’ user password, (2) locating the Grafana URL, and (3) setting up Port Forwarding:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*02-c81FtjdrRoj10" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*nHDgUQn7s04hjOZl" /></figure><p>**<strong>Note — If you do not properly setup port forwarding, you will not be able to reach the Grafana server!</strong></p><ul><li>With Grafana installed &amp; deployed, let’s proceed with installing Prometheus, with one simple command:</li></ul><pre>kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.12/samples/addons/prometheus.yaml</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*vAdGKv980cCE-65i" /><figcaption>All resources for Prometheus were created successfully</figcaption></figure><p><strong>Step 4</strong></p><p>Whew! I know there were a ton of steps above to implement, but the fun starts now as we get to play around with Grafana and the Prometheus data source, following some basic guidance below:</p><ul><li>Let’s start by taking a peek at the pods and services in the istio-system namespace:</li></ul><pre>kubectl get pods -n istio-system<br>kubectl get services -n istio-system</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*e448aZZnSnWyqVmB" /><figcaption>Active Pods Running</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*dAziA1Zesolf5OoW" /><figcaption>Active Services, along with the Cluster IP for each</figcaption></figure><ul><li>Since port forwarding was configured in an earlier step, we should be able to access Grafana, by navigating to <a href="http://localhost:3000/"><strong>http://localhost:3000</strong></a>, and logging in using the <strong>default</strong> user name of <strong>‘admin’</strong> and the<strong> </strong><strong>‘secret’</strong> that we generated above:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*YfSyEWbcr2rQYXau" /></figure><ul><li>We’ll be configuring a Prometheus data source, so go to “Configuration” &gt; “Data Sources” &gt; “Add data source”. Here, you will need to select Prometheus as the data source type, and set the URL to the Prometheus service:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*A06R5Xpycfum2ui_" /><figcaption>&lt;Cluster IP of Prometheus&gt;:9090</figcaption></figure><ul><li>You’ll need to <strong>“Save &amp; Test”</strong> the connection (located at the bottom left), and if the connection is successful, you will see these messages populate for the data source:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*V6wNFq9HzJnwAz7r" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Nav0jvTLWb1yAXxe" /></figure><ul><li>From here, you can play around with the various queries that are available to you, or create your own custom queries, along with creating a custom dashboard. If in fact your cluster is accessible and can be monitored via Grafana and Prometheus, once the queries have been added, and you have a dashboard created, you <em>should</em> see your results populate, like so:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*W6qmUFNNpO9RXWyf" /><figcaption>Notice the arrow points to the name of our Cluster &amp; NodeGroup created in Step 1; Cluster is accessible</figcaption></figure><ul><li>Additionally, you can <em>also</em> configure custom alerting (depending on your monitoring needs), which may look similar to the below:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*QnxmdN5Yy19KOH1Q" /><figcaption>No issues; Alerting Status Is Normal</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*ePbi5ipEkbNwQlxP" /><figcaption>Custom Group with a name of ‘EKSMesh’ created, along with Alert details and the current state</figcaption></figure><ul><li>Lastly, to wrap things up, let’s not forget about that custom webpage that we created at the very beginning of this tutorial. You should be able to reach your webpage via the gateway by either running <strong>http://&lt;cluster_ip&gt;/&lt;appname&gt;</strong> or <strong>localhost:&lt;containerport#&gt;</strong>:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*BcIBrnlrigCziZGQ" /><figcaption>Responsive Web Design Skills in Flight!</figcaption></figure><p>And there you have it 😁! We have successfully executed the following:</p><p>✅ Created an EKS Cluster &amp; NodeGroup; Also updated both!</p><p>✅ Created multiple deployments using a custom image</p><p>✅ Deployed an Istio gateway and virtual service</p><p>✅ Configured monitoring and alerting via Grafana &amp; Prometheus</p><p>✅ Verified that we could reach our custom webpage.</p><p>🚨Now, tear down EVERYYYTHIINNNGGGG as you do NOT want to leave resources running! Until next time 👋🏽!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9e6683325964" width="1" height="1" alt=""><hr><p><a href="https://levelup.gitconnected.com/aws-eks-observability-with-istio-grafana-prometheus-9e6683325964">AWS EKS: Observability with Istio, Grafana &amp; Prometheus</a> was originally published in <a href="https://levelup.gitconnected.com/">Level Up Coding</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Automating the Build of an NGINX Server via GitHub Actions, AWS ECR & CloudWatch]]></title>
            <link>https://levelup.gitconnected.com/automating-the-build-of-an-nginx-server-via-github-actions-aws-ecr-cloudwatch-6843ca283526?source=rss-6e1dbb973ea1------2</link>
            <guid isPermaLink="false">https://medium.com/p/6843ca283526</guid>
            <category><![CDATA[iam-roles]]></category>
            <category><![CDATA[aws-cloudwatch]]></category>
            <category><![CDATA[aws-ecr]]></category>
            <category><![CDATA[github-actions]]></category>
            <dc:creator><![CDATA[Katoria Henry]]></dc:creator>
            <pubDate>Sun, 14 Jan 2024 18:36:10 GMT</pubDate>
            <atom:updated>2024-03-20T12:06:58.323Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*h6b21GG5i8aOousf" /></figure><p>Hello World! I know its been a while since our last tutorial on leveraging AWS CodePipeline, but we’re staying in the groove of things with pipeline automation, but this time, via GitHub Actions! I know you’re probably wondering WHY I’ve chosen to use GitHub Actions for automation…and the answer is super simple — It’s one of the more simpler CI/CD tools that you can use, that doesn’t give you an automatic headache!</p><p>GitHub Actions is a continuous integration and continuous deployment (CI/CD) platform provided by GitHub. It allows you to automate various workflows, such as building, testing, and deploying your code, directly from your GitHub repository. It uses YAML-based configuration files called “workflow files” to define these automation tasks. The beauty behind it all is that you have the option to create your workflows directly in the UI, or if you’re feeling dangerous, you can actually build and test your workflows locally as well.</p><p>For this tutorial, we’re going to show you how to build, test, and analyze an NGINX image, using tools such as <a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/WhatIsCloudWatch.html">AWS CloudWatch</a> and <a href="https://aws.amazon.com/ecr/">AWS ECR</a> as our container registry (yes, our pipeline will also include Docker references!). We’ll be creating our container registry for ECS, as those two services are normally used in tandem. Though we won’t be creating a cluster, I think it’s beneficial to show how ECS/ECR are used together. Now with CloudWatch, we are simply going to set up some basic logging. Nothing too fancy, but it’s an excellent service to use for monitoring, logging, and gaining insights into activity for your servers and other resources. If you’re interested in following along with this tutorial and giving it a go, check out the pre-reqs that I have below and let’s get started!</p><p><strong>Pre-Reqs</strong></p><ul><li><a href="https://github.com/join">GitHub Account</a></li><li><a href="https://aws.amazon.com/free/?gclid=Cj0KCQiAhomtBhDgARIsABcaYylVXa8vEfcqcsANx8YfD6dQlV0f5ywljSSZPkt9kdkBsjRzaXvxvD8aAmD9EALw_wcB&amp;trk=7541ebd3-552d-4f98-9357-b542436aa66c&amp;sc_channel=ps&amp;ef_id=Cj0KCQiAhomtBhDgARIsABcaYylVXa8vEfcqcsANx8YfD6dQlV0f5ywljSSZPkt9kdkBsjRzaXvxvD8aAmD9EALw_wcB:G:s&amp;s_kwcid=AL!4422!3!651751058790!e!!g!!aws%20account!19852662149!145019243897&amp;all-free-tier.sort-by=item.additionalFields.SortRank&amp;all-free-tier.sort-order=asc&amp;awsf.Free%20Tier%20Types=*all&amp;awsf.Free%20Tier%20Categories=*all">AWS Account</a></li><li>IAM rules/policies configured for the necessary AWS resources</li><li><a href="https://docs.docker.com/engine/install/">Docker installed</a> (Can use Rancher desktop, install Docker locally, or install it on an EC2 instance — your choice!)</li><li><em>Some</em> working knowledge of <a href="https://docs.github.com/en/actions">GitHub Actions</a></li></ul><h3><strong>Part 1</strong></h3><ul><li>One of the most important steps for this tutorial is to ensure that you are NOT hardcoding any of your credentials within your GitHub Actions pipeline. It is ideal to leverage either a secrets manager, such as AWS Secrets Manager, Vault, or you can actually add your secrets to your repository, such as what I’ve done for this demo. Note: <em>Your secrets may differ as your pipeline may not include certain values that I’m passing in my pipeline.</em></li><li>For your repository, navigate to “<strong>Settings → Security → Secrets &amp; Variables → Actions</strong>” and this is where you will place your secrets.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/878/0*gszaXsvqUZ5odCdi" /></figure><ul><li>As a best practice, it is ALWAYS essential to include branch protection rules for your repository. Note that if you are using a private repository for the FREE version of GitHub, you will not be able to add Branch protection rules. You will have to use a Public repo for this.</li><li>The next step is to clone the repository that you’ve created above locally. This is needed as you will be adding a new directory and the required files for your pipeline. To clone your repository, you would Navigate to your <strong>repo → Code → Select your clone method</strong>:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/920/0*8ClUntTghbxMBiwC" /></figure><h3><strong>Part 2</strong></h3><ul><li>If you have not already done so, you will need to create a new directory locally, and change into that directory BEFORE you actually paste your URL code for your repo. To make a new directory and change into that directory, you’d simply run mkdir &lt;directoryname&gt; &amp;&amp; cd &lt;directoryname&gt;.</li><li>Remember to add ‘git clone’ before pasting your URL. You will also need to change into the repo directory after it is cloned:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*HdF9rKJnCxRCFv-S" /></figure><ul><li>Next, we will need to create our Dockerfile (for the Nginx image we intend to use) as well as the Nginx configuration file. These are files that can be standalone or placed in a directory (optional).</li><li>Your Dockerfile should include the image you intend to pull, a command to copy the NGINX config file, port exposure, and a command to start the NGINX server. Remember, the less lines used within your Dockerfile, the better, as this helps with keeping the image size small.</li><li>You’ll also need a script to create your Log Group &amp; Log Stream for AWS CloudWatch:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*1yAa3aEqFeqnJXZ0" /><figcaption>NGINX Dockerfile</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Vgk08iuzlNfUl-ze" /><figcaption>NGINX Config File</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8TNH14ruIAD3jTSA4ypR6w.png" /><figcaption>logs.sh file (bash script)</figcaption></figure><h3><strong>Part 3</strong></h3><ul><li>Next, let’s create our directory for our workflow, as well as create an empty yaml file. As an aside, Workflows are automated procedures that you can set up in your GitHub repository to build, test, package, release, or deploy any code project on GitHub.The YAML file specifies the events that trigger the workflow, such as a push or pull request to the repository, or on a schedule (like a cron job). Your directory MUST begin with <strong>.github/workflows:</strong></li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*1C1B98pHd85bpodN" /></figure><ul><li>With our empty file created, we will now build out our workflow. A few things that we will include are the following:</li></ul><p><strong>On-push</strong>: This is the event that we are selecting to <strong>trigger our workflow </strong>which means it will run automatically whenever a change is ‘pushed’ to that repo.</p><p><strong>Matrix Strategy</strong>: This will allow us to run jobs across multiple combinations of environments, operating systems, programming languages, or any other set of variables.</p><p><strong>Jobs</strong>: We will have a job for building the nginx image, testing the image, and a job for using AWS CloudWatch. Jobs will be running on a runner machine and will execute the individual steps. A ‘runner’ can be self-hosted (created by you), or you can leverage existing runners (github managed). They are simply operating systems.</p><p><strong>Steps</strong>: Steps are located within each individual job and will either perform an action directly, or will leverage a script or our choice</p><p><strong>Actions</strong>: These are pre-defined by you or pulled from the GitHub Marketplace. They are basically standalone commands that are combined into steps to create a job. They checkout your repo under your Github workspace so that your workflow can access it.</p><p>**Note: When you have multiple jobs in a workflow, you must specify within that job if it “needs” the previous job to be completed before it will start.</p><ul><li>With the explanation above that gives a more detailed breakdown of the workflow, let’s see how it all comes together in our yaml file (our pipeline):</li></ul><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/12856ca3e06d79510ebe3e18cb28e22b/href">https://medium.com/media/12856ca3e06d79510ebe3e18cb28e22b/href</a></iframe><h3><strong>Part 4</strong></h3><p>Now that we have all of the necessary files that we need for our deployment, let’s proceed by adding the files to our repo and watching the magic unfold!</p><ul><li>Start by running the command <strong>git add .</strong></li><li>Next, run <strong>git commit -m &lt;yourcommitmessage&gt;</strong></li><li>Finally run <strong>git push</strong> and head to the “Actions” tab of your repo</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*WpxypfMnGo6QE_b7" /></figure><ul><li>In the Actions tab, you’ll notice that the workflow has started as we chose the ‘push’ event trigger, so it was automatically started on our behalf (<em>You can also manually run the workflow over directly within the consol</em>e)</li><li>Take a peek at the various steps in your workflow to review the steps that are being executed:</li></ul><p>Our Pipeline shows a complete success!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*lXQvO8GRBzcpQzYI" /></figure><p>ECS/ECR Job</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*idrQJDBVuVC54HJU" /></figure><p>Build &amp; Push</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*AAZqYMCBwezrEwAq" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*OWmfzv3BbXlIYjXK" /></figure><p>Image Testing</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/726/0*koJwLI_WQBhzZOK1" /></figure><p>CloudWatch Log Creation</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1008/0*5EOAAq1mHxvwkI1Z" /></figure><ul><li>As a bonus, you can navigate to your AWS Console and review the goodies that were created via your pipeline as well:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*09ZJxCLvdly5AzWD" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*d9iIfVToDXIz6mZY" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*gk1CoPRDD1gjDN3T" /></figure><ul><li>*Remember, customizing your pipeline requires reviewing the GitHub Actions documentation, and adding in your explicit customizations and actions, based on your deployment. As mentioned earlier, NEVER hard-code your credentials and to ALWAYS test your pipelines on a branch other than main before merging with your codebase. That is the biggest takeaway from this tutorial. I hope you all enjoyed it and remember to delete the resources you no longer need!! See ya next time 😀!</li></ul><p>Follow me on LinkedIn: <a href="https://www.linkedin.com/in/katoria-henry-2018/">https://www.linkedin.com/in/katoria-henry-2018/</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6843ca283526" width="1" height="1" alt=""><hr><p><a href="https://levelup.gitconnected.com/automating-the-build-of-an-nginx-server-via-github-actions-aws-ecr-cloudwatch-6843ca283526">Automating the Build of an NGINX Server via GitHub Actions, AWS ECR &amp; CloudWatch</a> was originally published in <a href="https://levelup.gitconnected.com/">Level Up Coding</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Into the Storm With Terraform & GitLab]]></title>
            <link>https://levelup.gitconnected.com/into-the-storm-with-terraform-gitlab-15c9b6c9aa53?source=rss-6e1dbb973ea1------2</link>
            <guid isPermaLink="false">https://medium.com/p/15c9b6c9aa53</guid>
            <category><![CDATA[automation]]></category>
            <category><![CDATA[terraform]]></category>
            <category><![CDATA[gitlab]]></category>
            <category><![CDATA[cicd-pipeline]]></category>
            <category><![CDATA[docker]]></category>
            <dc:creator><![CDATA[Katoria Henry]]></dc:creator>
            <pubDate>Sat, 25 Feb 2023 16:40:27 GMT</pubDate>
            <atom:updated>2023-02-27T00:20:25.703Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*s4HbiDsbiIUVqn6ntWNPiw.png" /><figcaption>Image Credit: Tamir Gefen</figcaption></figure><p>For the past several weeks I’ve taken you all on a miniature Terraform journey, and that continues today as well! This week, I’m going to be walking you all through a very simple project of using Terraform with GitLab 🙂. Last week we took a look at another CI/CD tool, Terraform Cloud, in which we only used the Terraform modules and files that we created to deploy our resources. This week, we’re going to take it up a notch and build a pipeline using Terraform files, as well as a customized GitLab yaml file for our AWS deployment!</p><p>So what exactly is GitLab? Well, GitLab has become more and more popular these days as companies look for simplified, secure, and CI/CD tools to help with deployments. If you understand the basics of Git, which is used for version control and tracking changes to files, well add in the “Lab” part to that and you have a web-based Git repo that serves as a CI/CD tool, provides open repos for collaboration across teams, and like Git, provides an easy method for tracking changes within a project.</p><p>Additional interesting facts about GitLab include the following:</p><ul><li>It helps with automating the entire DevOps lifecycle</li><li>Considered one of the most user-friendly DevOps platform for project planning and execution</li><li>Some of its key features include issue tracking, automation for building and testing code, as well as security enhancements such as scanning pipelines and workflows for vulnerabilities</li><li>Gitlab provides end-to-end DevOps and DevSecOps capabilities</li><li>You have the option of choosing between public or private branches (FREE)</li><li>GitLab also offers online code storage</li></ul><p>So now that we have an overview of GitLab, let’s get into our scenario for this walkthrough. Again, we will keep it very simple with our Terraform code this week and the resources to deploy:</p><p>Your team needs you to deploy a Docker container with a Centos image:</p><ul><li>Pull a Centos image from the Docker registry</li><li>Create an AWS ECS cluster using the docker image with Terraform</li><li>Create a remote backend to store your Terraform State file in a S3 Bucket</li><li>Use a CI/CD pipeline tool that will deploy the infrastructure through AWS</li><li>Add a build stage to your pipeline to test before deploying your code</li></ul><p><strong>Resources/PreReqs:</strong></p><ul><li>As always, Confidence to get it done!</li><li>AWS Account</li><li>GitHub Account integrated with VSCode or Cloud9</li><li>Terraform Installed</li><li>GitLab Account: Sign up <a href="https://about.gitlab.com/">Here</a></li><li>AWS IAM generated Access/Secret Key</li></ul><p><strong>Part 1</strong></p><p>In this section, I’ve included a few of the files that we will be using for today’s deployment. Instead of using several modules as with my previous tutorials, I opted to use single files this time around instead. Feel free to clone my Github repo found here for all files we will be using for the walkthrough:</p><p><a href="https://github.com/theCaptN21/CICD-Tooling-Terraform">GitHub - theCaptN21/CICD-Tooling-Terraform: Repo that includes Terraform files for an automated workflow using CI/CD tools</a></p><p>ecs.tf (Main.tf)</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/0d18a9eee6df5f4bcb23d741c18c39ff/href">https://medium.com/media/0d18a9eee6df5f4bcb23d741c18c39ff/href</a></iframe><p>.gitlab-ci.yml (<strong>Be sure to customize this based on your deployment reqs</strong>)</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/8c908e02f3428b0b7b182dbf3414e975/href">https://medium.com/media/8c908e02f3428b0b7b182dbf3414e975/href</a></iframe><p>Variables.tf</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/42d20796cc7a3fd532906b468b28514c/href">https://medium.com/media/42d20796cc7a3fd532906b468b28514c/href</a></iframe><p><strong>Part 2</strong></p><p>Let’s walk through getting our Project set up in GitLab as this phase may confuse some. If you’ve already signed up for your free trial of GitLab and you’re logged into your Github account, follow the steps below:</p><ul><li>In GitLab, the first thing that you want to do is to configure your CI/CD to run with an external repo (Your GitHub Repo or the URL for the Repo):</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*vP3NZlRV7EfEhP9p" /></figure><ul><li>If you haven’t already done so (I hadn’t used GitLab in a while), you may be required to “Authenticate” first, before you can actually connect your repo:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*qStfRuFFJa3omeqt" /></figure><ul><li>Once you’ve authenticated, you’ll be promoted to “Import” the repositories that you’d like to work on for the project. Be sure to only select the repos that you absolutely <strong><em>NEED</em></strong> to import:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*0dFoNo2J-Y4UMpBm" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*33thgod3FdVl4vs3" /></figure><ul><li>Once the repo has been successfully imported, it should look similar to this:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SRHDDKpsOwWSeC6lwe4c-g.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PSG1poHAtf2-kO_fms721Q.png" /></figure><ul><li>Similar to what was done in Terraform Cloud, you must also configure your AWS environment variables to ensure you can deploy the resources required. Navigate to “<strong>Settings -&gt; CI/CD -&gt; Variables (Expand) -&gt; Add variable</strong>”, and populate the fields for REGION, ACCESS &amp; SECRET Key (be sure to <strong><em>mask</em></strong> these data fields):</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/940/1*Ve3e8YSHKTfnzjf_UGRamQ.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*EzSdfTfqzu3t5k_mSv-h0g.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pMn3CnYdSEgijc9RgcNbzQ.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YMt258La7fP5VTGM1miHaw.png" /></figure><p><strong>Part 3</strong></p><p>In this next section, we will walk through the steps of building the actual GitLab yaml file required for our Terraform deployment. Though my file was created prior to the deployment, it’s helpful in understanding how I got to that point:</p><ul><li>Within GitLab, navigate to the main menu and there should be an option that provides a list of templates that you can use to start your deplyments. Since we will be using Terraform, let’s select that Template :</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*l5fCFbWPdmFPpr_L1QH6bA.png" /></figure><ul><li>With our template selected, we can begin to either keep the file as is, or customize it based on our deployment workflow. The template will look like this and will tell you if your syntax is accurate. It will prompt you to push your newly created template to your repo once completed:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*53wdw4pE_8Kb3j4Z4fR8mA.png" /></figure><ul><li>Getting back into the swing of things, we can now head over to “CI/CD -&gt; Pipelines -&gt; Run Pipeline (2x)”, to watch the magic unfold:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-JzQpA7mZvsTfjkLswqPsA.png" /></figure><p><strong>Part 4</strong></p><p>With our pipeline now kicking off, let’s cross our fingers, toes, legs, and anything else we can in hopes that it goes through lol!!</p><ul><li>You won’t believe it, but I received an error initially when I tried my first Pipeline run. It is quite embarrassing to say the least lol:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jHm3IF0J6u6DnH0GbEYKEA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_ZKpmC7IxqK9OOWo_le7gw.png" /></figure><ul><li>As you can see from above, I “forgot” to validate my account — Rookie mistake!! But once validated, I was able to proceed with the run, and this is how it went:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*iVzQKXqn8m7aLANGNBjujw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RBfy23kRfMNW5TNLDhc1nA.png" /><figcaption>The “Apply” phase initially failed due to a typo in my template, but once corrected, it went through!</figcaption></figure><ul><li>Typically, it’s a good idea to actually check out the “jobs” in GitLab as the pipeline is being built, to ensure you’re seeing the resources being created and <strong>green</strong> throughout. If all goes well, you will see “Job Succeeded” at the end of the build:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Rgj-uSk5MVxMRTQV60_hmA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*z2FELdlwyw_KFrLcjjS2Tw.png" /></figure><p><strong>Part 5</strong></p><p>While I was able to get the pipeline running and the jobs were successful, I, myself, need to brush up on my GitLab skills! Too many rookie errors, forgetting syntax reqs, etc. I will be taking it up a notch with the next walkthrough for sure! Finally, let’s head over to AWS to check out the resources that were created:</p><p>Cluster &amp; Service</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bQguHeY-f7a49IYRLxGGbA.png" /></figure><p>S3 Bucket</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GnPN9e8USNiQta0Y24w2zA.png" /></figure><p>VPC w/ Route Tables &amp; Subnets</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ximAX70xzuNWaeMsWn7FCw.png" /></figure><p>Docker Container (Centos)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gPUfCCL8SMZTaSkenjXt3w.png" /></figure><p>Capacity Providers</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MSM3vB6bD-UabR7McCe9Sg.png" /></figure><ul><li><strong>Note:</strong> If you did not set your gitlab-ci.yml file to auto destroy your resources, be sure that you select “Manual” when it gets to that phase in the pipeline to destroy unneeded resources!!!</li></ul><p><strong>BONUS:</strong> For a quick tutorial on using GitLab to manage your Terraform files, check out this video below:</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FiMqeu9XDTxI%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DiMqeu9XDTxI&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FiMqeu9XDTxI%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/478c6271750612a687bd5ab0e4cdb5e2/href">https://medium.com/media/478c6271750612a687bd5ab0e4cdb5e2/href</a></iframe><p>Follow me on <a href="https://www.linkedin.com/in/katoria-henry-2018">LinkedIn</a>, and <a href="https://github.com/theCaptN21">@theCaptN21</a> on GitHub!</p><h3>Level Up Coding</h3><p>Thanks for being a part of our community! Before you go:</p><ul><li>👏 Clap for the story and follow the author 👉</li><li>📰 View more content in the <a href="https://levelup.gitconnected.com/?utm_source=pub&amp;utm_medium=post">Level Up Coding publication</a></li><li>💰 Free coding interview course ⇒ <a href="https://skilled.dev/?utm_source=luc&amp;utm_medium=article">View Course</a></li><li>🔔 Follow us: <a href="https://twitter.com/gitconnected">Twitter</a> | <a href="https://www.linkedin.com/company/gitconnected">LinkedIn</a> | <a href="https://newsletter.levelup.dev/">Newsletter</a></li></ul><p>🚀👉 <a href="https://jobs.levelup.dev/talent/welcome?referral=true"><strong>Join the Level Up talent collective and find an amazing job</strong></a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=15c9b6c9aa53" width="1" height="1" alt=""><hr><p><a href="https://levelup.gitconnected.com/into-the-storm-with-terraform-gitlab-15c9b6c9aa53">Into the Storm With Terraform &amp; GitLab</a> was originally published in <a href="https://levelup.gitconnected.com/">Level Up Coding</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Continuous Integration and Continuous Deployment with Terraform Cloud]]></title>
            <link>https://levelup.gitconnected.com/continuous-integration-continuous-deployment-with-terraform-cloud-ad384f29d7a0?source=rss-6e1dbb973ea1------2</link>
            <guid isPermaLink="false">https://medium.com/p/ad384f29d7a0</guid>
            <category><![CDATA[automation]]></category>
            <category><![CDATA[terraform]]></category>
            <category><![CDATA[ci-cd-pipeline]]></category>
            <category><![CDATA[aws]]></category>
            <dc:creator><![CDATA[Katoria Henry]]></dc:creator>
            <pubDate>Sat, 18 Feb 2023 19:47:09 GMT</pubDate>
            <atom:updated>2023-02-20T20:33:07.157Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*gw49k4ZyQ6bFMIk1" /></figure><p>This week we’re coming right back at you with more Terraform, in which we’ll be leveraging Terraform Cloud as our CI/CD tool to verify our build. If you’ve never heard of Terraform Cloud, one of the top reasons as to why it’s used in DevOps is AUTOMATION — from compliance, to management of various cloud service providers, data centers, to infrastructure automation for provisioning, Terraform Cloud does that for you. It’s basically a SaaS that could be run locally and enables Terraform to retrieve and save state files from it as well.</p><p>Additional fun facts about Terraform Cloud are as follows:</p><ul><li>You have visibility into state files that are being accessed and by whom</li><li>Versioning and backing up state files is as easy as 1,2,3</li><li>You can talk directly to various CSPs (remote state)</li><li>You can set up your runs so that they are approved before deployment; There’s only one run at a time</li><li>And of course, it improves overall collaboration</li></ul><p>*I would highly recommend that you check out some of the tutorials for Terraform Cloud to become more familiar with it. They can be found<a href="https://developer.hashicorp.com/hcp/tutorials?ajs_aid=60bdd68f-883b-4abc-bab9-c36f27e58a32&amp;product_intent=terraform&amp;utm_source=tf_cloud&amp;utm_campaign=tf_cloud"> here</a>.</p><p>Today we’ll be deploying multiple AWS resources via Terraform Cloud to see some of the bells and whistles that it has. For starters, it’s best to get your Github repo all set up as that will be needed for this walkthrough. Similar to what we did last week, we will be leveraging modules this week as well, and this is the scenario we will attempt to execute:</p><ul><li>Create a highly available two-tier AWS architecture containing the following:</li><li>Custom VPC with:</li></ul><p>2 Public Subnets for the Web Server Tier</p><p>2 Private Subnets for the RDS Tier</p><ul><li>Launch an EC2 Instance with your choice of Web Server in each private web tier subnet (Apache, NGINX, etc).</li><li>One RDS MySQL Instance (micro) in the private RDS subnets</li><li>Security Groups properly configured for resources ( Web servers, RDS)</li><li>Use module blocks for ease of use and re-usability</li><li>Replace the EC2 Instances with an Auto Scaling Group for the Web Server (in the Public Web Server subnets) with min of 2 and max of 5 spread across the 2 subnets</li><li>Internet-facing Application Load Balancer targeting the Web Server Auto Scaling Group.</li><li>ALB Security Group with permissions and modifications required for the Web Servers SG to reflect the new architecture</li><li>Use Terraform Cloud as the CI/CD to check the build</li></ul><p><strong>Note</strong>: <em>I’ve done a dry run of this locally and I was having issues resolving these </em><strong><em>two</em></strong><em> errors below 🥴, so we’ll see if we can get it fixed using Terraform Cloud!</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*qnlLmURDALYa8cX2" /></figure><p><strong>Resources/PreReqs:</strong></p><ul><li>As always, Confidence to get it done!</li><li>AWS Account</li><li>GitHub Account integrated with VSCode</li><li>Terraform Installed</li><li>Terraform Cloud Account: Sign up <a href="https://cloud.hashicorp.com/products/terraform">Here</a></li><li>Source Code Editor (I use Visual Studio Code (VS Code)) w/ the Terraform Extension Installed</li><li>AWS IAM generated Access/Secret Key</li></ul><p><strong>Part 1</strong></p><p>If you haven’t already done so, be sure to sign up for Terraform Cloud, preferably using your GitHub account as that will be needed for this walkthrough. I actually used my email when I first signed up for my account, so no biggie if you choose not to sign up using Github. Because there are various steps that we’ll be walkthing through, let’s highlight a few of the files that we will be using today:</p><p>Main.tf (root)</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/9907f25494bf4616bf1caec9ca794d1f/href">https://medium.com/media/9907f25494bf4616bf1caec9ca794d1f/href</a></iframe><p>Compute (main.tf)</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/6c60857a360b9e277ceea487c077051b/href">https://medium.com/media/6c60857a360b9e277ceea487c077051b/href</a></iframe><p>Load Balancer (main.tf) — Be sure to add your VPC ID to line 17</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/40e2d8c8a9e0c65f52e06522b1d29c2f/href">https://medium.com/media/40e2d8c8a9e0c65f52e06522b1d29c2f/href</a></iframe><p>Your file structure should resemble this once completed:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/696/0*1lagMaMwSSEFgkfn" /></figure><p>For a full list of files and modules that will be used, feel free to clone my repo for this walkthrough that can be found here:</p><p><a href="https://github.com/theCaptN21/Terraform-Cloud">GitHub - theCaptN21/Terraform-Cloud: TF Modules to be used w/ Terraform Cloud</a></p><p><strong>Part 2</strong></p><p>Now that we have signed up for Terraform Cloud, let’s start with some of the basics of getting things set up for our new deployment. Follow the steps below to kick things off:</p><ul><li>As previously mentioned, if you’ve signed up using your Github account, select “<strong>Sign in with Github</strong>”, as shown below. Otherwise, continue with your email and password:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/523/0*PqIG6gI9bibp4jO4" /></figure><ul><li>If you signed up for MFA, you may be prompted to enter the 6-digit code that’s provided before being redirected</li><li>Now that you’ve successfully logged on, you’ll see “<strong>Projects &amp; Workspaces</strong>”, in which for most will be empty if this is your first time using it:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*q7fAdl1Ny9hlNbiU" /></figure><ul><li>Since this is a new deployment, let’s start by selecting “<strong>New Workspace</strong>”, in which you will then select “<strong>Version control workflow</strong>”</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/956/0*S2PTw7Ulc-HRvmer" /></figure><ul><li>Connect to Github (or if you’re using one of the other version providers, select that option). Select “<strong>Github.com</strong>”, and then authorize Terraform Cloud. From there, you’ll want to select the repositories that will be used for this walkthrough:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/817/0*nEoD9uGzMNYHJo6I" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/802/0*0cF9wwlLmgyB_mPZ" /></figure><ul><li>Be sure to give the Workspace a name and pay close attention to the advanced options. If you’re like me and would prefer NOT to have changes automatically pushed to your repo and/or applied to your configuration automatically (though auto apply is more convenient), keep your apply method as “<strong>Manual”</strong>, and then select “<strong>Create workspace</strong>”</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*2SFisRrFkGmdwm6L" /></figure><ul><li>We’ll have to configure our variables next (this will be your AWS access/secret key that were a pre-req), so let’s click on “<strong>Continue to workspace overview</strong>” before starting our new plan. Start by clicking on “<strong>Configure Variables</strong>”, and you will add a <strong>Terraform variable</strong> for access to the Bastion Host. Following this, you will add the<strong> Environment variables</strong> for AWS, and mark them as Sensitive to prevent the actual value from being displayed:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*3HvcfgckYJkQZvnb" /></figure><p><strong>Part 3</strong></p><p>Previous versions of Terraform Cloud would require you to start a new plan immediately after adding in your required variables, but with that option being changed, we will proceed with our first step of <strong>starting a new run</strong>, as shown below:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/507/0*7Rexbsmvek8-Ir0R" /></figure><ul><li>Let’s give our run a name and leave the default run type as is for now, and click on “<strong>Start Run</strong>”:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/621/0*ltoSSWopbjRyB_rd" /></figure><ul><li>If you’re seeing “GREEN” on your end, that means your configuration was valid and Terraform Cloud will be able to create the resources as listed. To continue, select “<strong>Confirm &amp; Apply”</strong>, followed by “<strong>Confirm Plan</strong>”, and your AWS resources will be created:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*hGl1yhJhd10q2wqb" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/755/0*Zq_6ksEeXjYSJhsS" /></figure><ul><li>Out of the 33 resources that we were hoping to create, it appears as though we have the SAME two errors from our dry-run that was completed earlier ☹️:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*jW9gm7RGlK3e8hYO" /></figure><ul><li>All other resources were successfully created, so let’s see if we can fix our <strong>Load Balancer Module</strong> to get rid of one of the two errors. We’ll start by removing “var.vpc_id”, and applying the VPC-ID to the target group directly:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/840/0*W55smJytash1OuUY" /></figure><ul><li>Stage your changes to this file, and then push to Github. It should automatically trigger a new run in Terraform Cloud:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/915/0*nxI_GiFPg3qQqvEK" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*X9GmcqXRLHJMczeb" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*qWz8u8F8PXNlJo0g" /></figure><ul><li>Now, let’s <strong>retry our run</strong> to see what happens:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*g5q00Ir9zCM722qE" /></figure><ul><li>It appears as though we cleared one error, but we are still having issues with that autoscaling group for RDS…let’s try one more update to see if we can resolve the error. We will follow the exact same steps and make updates to our “<strong>Compute</strong>” variables.tf file. It’s showing that our instance type is invalid for the launch template, which is an easy fix:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*LPHVp10wERs9lizY" /><figcaption>It worked! (Okay I knew that it would but still wanted to create some errors)</figcaption></figure><ul><li>You’ll also notice that the output provided us with our alb_dns address, so let’s copy that and paste it into our browser, to see what happens:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*4NKhWe0TmDIs6WGC" /></figure><ul><li>After waiting for roughly 10 min, I gave it another shot to access the Application Load Balancer, and…New Error</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Ul6CQCxrpBpdDTQb" /></figure><p><strong>Part 4</strong></p><p>With our final error now being resolved, we can head into our AWS Console to verify some of the resources that were created:</p><p>VPC</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*JXXjw7KjSKyK9UFM" /></figure><p>Load Balancer across 3AZs</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*f8Jyd2Q7DazYJeYM" /></figure><p>Internet Gateway</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*uvNBbHJVpriESQfK" /></figure><p>Auto Scaling Groups</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*t5MrlKZEIWywVjrg" /></figure><p>EC2 Connect</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*cj5Xfi5-J9mTDr-A" /></figure><p>Terraform Cloud List of Resources Created</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*3pmyLqREtpxTWcvl" /></figure><p><strong>Part 5</strong></p><p>I hope you’ve enjoyed using Terraform Cloud CI/CD for your AWS resources. We’ve succeeded, failed, and had several “eye-brow-raising” moments with this walkthrough, but we made to the end! If you’re ready to destroy everything that you’ve deployed, let’s start with the steps below:</p><ul><li>Head over to your workspace settings, and scroll down to “<strong>Destruction and Deletion</strong>”</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*PsYtIU8z_E5LD2i2" /></figure><ul><li>Select “<strong>Queue Destroy Plan</strong>”, and confirm your Workspace Name</li><li>And one-by-one, you will start to see the resources being destroyed:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*LSArvtUkCWZdP1Vi" /></figure><ul><li>Remember, you must select “<strong>Confirm &amp; Apply</strong>”, followed by “<strong>Confirm Plan</strong>” to ensure the resources are deleted:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*g6l5LJMyKT7AdvUt" /></figure><p>As a best practice, <strong>double check</strong> that the resources no longer exist by visiting your AWS Console as well! And that wraps it up for this week 👏</p><p>Until next time everyone! Follow me on <a href="https://www.linkedin.com/in/katoria-henry-2018">LinkedIn</a>, and <a href="https://github.com/theCaptN21">@theCaptN21</a> on GitHub!</p><h3>Level Up Coding</h3><p>Thanks for being a part of our community! Before you go:</p><ul><li>👏 Clap for the story and follow the author 👉</li><li>📰 View more content in the <a href="https://levelup.gitconnected.com/?utm_source=pub&amp;utm_medium=post">Level Up Coding publication</a></li><li>💰 <a href="https://skilled.dev/?utm_source=luc&amp;utm_medium=article">FREE coding interview course</a></li><li>🔔 Follow us: <a href="https://twitter.com/gitconnected">Twitter</a> | <a href="https://www.linkedin.com/company/gitconnected">LinkedIn</a> | <a href="https://newsletter.levelup.dev/">Newsletter</a></li></ul><p>🚀👉 <a href="https://jobs.levelup.dev/talent/welcome?referral=true"><strong>Join the Level Up talent collective and find an amazing job</strong></a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ad384f29d7a0" width="1" height="1" alt=""><hr><p><a href="https://levelup.gitconnected.com/continuous-integration-continuous-deployment-with-terraform-cloud-ad384f29d7a0">Continuous Integration and Continuous Deployment with Terraform Cloud</a> was originally published in <a href="https://levelup.gitconnected.com/">Level Up Coding</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>