<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.2">Jekyll</generator><link href="https://tty.neveragain.de/feed.xml" rel="self" type="application/atom+xml" /><link href="https://tty.neveragain.de/" rel="alternate" type="text/html" /><updated>2024-05-21T06:48:30+02:00</updated><id>https://tty.neveragain.de/feed.xml</id><title type="html">neveragain.de teletype</title><subtitle>A blog about IT infrastructure and AWS</subtitle><entry><title type="html">AWS: Egress Traffic and Using AWS Services via IPv6</title><link href="https://tty.neveragain.de/2024/05/20/aws-ipv6-egress.html" rel="alternate" type="text/html" title="AWS: Egress Traffic and Using AWS Services via IPv6" /><published>2024-05-20T00:00:00+02:00</published><updated>2024-05-20T00:00:00+02:00</updated><id>https://tty.neveragain.de/2024/05/20/aws-ipv6-egress</id><content type="html" xml:base="https://tty.neveragain.de/2024/05/20/aws-ipv6-egress.html"><![CDATA[<hr />

<p><img src="https://tty.neveragain.de/assets/cyberroad-400.jpeg" alt="a road away from here" style="float: left; margin-right: 1em" width="400" height="400" /></p>

<h2 id="introduction">Introduction</h2>

<p>In February 2024, AWS started charging customers for public IPv4 addresses. I recommend reading my <del>rant</del> analysis of the AWS announcement
first: <a href="/2023/09/21/aws-cannot-escape-ipv4.html">Cannot Escape IPv4</a>. The
second part of this series explored the situation for <a href="/2023/10/24/aws-ipv6-ingress.html">ingress traffic</a>.</p>

<p>This final part will explore options for egress traffic from VPCs, with a focus on using IPv6 to avoid public IPv4 addresses entirely
(spoiler: not feasible).</p>

<p>Egress connectivity “to the internet” is required not only for downloading software and
container images, but also for accessing other services like Github, payment providers, sending telemetry, etc. –
and for using AWS services: EC2 instances might access objects
on S3, containers might send messages to SQS queues, and so on.</p>

<p>And as connecting to AWS service endpoints via IPv6 is surprisingly involved, I will take a look at the different
kinds of endpoints and the additional configuration required for the AWS CLI and SDKs.</p>

<hr />

<h2 id="egress-from-vpc-resources">Egress from VPC Resources</h2>

<h3 id="options-for-egress">Options for Egress</h3>

<p>With IPv4, the common patterns for enabling egress traffic from VPCs are:</p>

<ul>
  <li>Public subnet: Route to an <a href="https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Internet_Gateway.html">Internet Gateway</a>;
direct assignment of public IP addresses to VPC resources like EC2 instances and containers</li>
  <li>Private subnet: Using only private addresses on resources, but adding a
<a href="https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html">Managed NAT Gateway</a>
(or a cheaper self-managed NAT Instance<sup id="fnref:fcknat" role="doc-noteref"><a href="#fn:fcknat" class="footnote" rel="footnote">1</a></sup>) that translates all egress connections to a single public IPv4 source address</li>
  <li>PrivateLink: Even when a subnet does not have any internet connectivity (internal subnet), it’s possible to enable egress to specific AWS services
by adding <a href="https://docs.aws.amazon.com/vpc/latest/privatelink/privatelink-access-aws-services.html">Interface Endpoints</a>
(PrivateLink) or Gateway Endpoints<sup id="fnref:gwendpoint" role="doc-noteref"><a href="#fn:gwendpoint" class="footnote" rel="footnote">2</a></sup> to the VPC</li>
</ul>

<p>This is mostly the same with IPv6, but there is one significant difference.</p>

<h3 id="no-more-nat-gateway">No More NAT Gateway</h3>

<p>With IPv6, there is no NAT<sup id="fnref:nat" role="doc-noteref"><a href="#fn:nat" class="footnote" rel="footnote">3</a></sup>. Every IPv6 address used in a VPC is
from a public address block. The difference between a public and a private subnet would no longer exist.</p>

<p>To mimic the familiar security characteristics of private subnets with NAT, AWS offers the
<a href="https://docs.aws.amazon.com/vpc/latest/userguide/egress-only-internet-gateway.html"><em>Egress-only</em> Internet Gateway</a> for IPv6:
It works like the regular Internet Gateway, but it only allows egress connections, i.e. it will never allow connections from the
internet to VPC resources.</p>

<p>And just like Internet Gateway, it doesn’t cost anything. It has no fixed hourly price, it doesn’t have additional traffic-based charges,
and it doesn’t require a public IP address that adds to its price – all of which a NAT Gateway does.
IPv6 can be a cost optimization measure!</p>

<h3 id="dual-stack-and-ipv6-only-subnets">Dual-Stack and IPv6-Only Subnets</h3>

<p>IPv6-enabled VPC subnets will usually be dual-stack, meaning IPv6 will be configured in addition to IPv4 (running two network
protocol stacks). Any resource running in
dual-stack subnets will pick up an IPv4 address as always, and can additionally pick up an IPv6 address.</p>

<p>It’s possible to create
<a href="https://aws.amazon.com/blogs/networking-and-content-delivery/introducing-ipv6-only-subnets-and-ec2-instances/">IPv6-only subnets</a> that have no
IPv4 configuration at all, avoiding the complexity of running two network protocols.
I love that this is possible, and I hope this will be the default mode of operation at some point.
And if the VPC also has public IPv4 and a Managed NAT Gateway,
<a href="https://docs.aws.amazon.com/vpc/latest/userguide/nat-gateway-nat64-dns64.html">DNS64 and NAT64 support</a>
enables IPv6-only resources to seamlessly access to IPv4-only targets.</p>

<h3 id="ipv6-egress-support-by-service">IPv6 Egress Support by Service</h3>

<p>So far, IPv6-<em>only</em> subnets are only supported by EC2 instances built on
<a href="https://docs.aws.amazon.com/ec2/latest/instancetypes/ec2-nitro-instances.html">Nitro</a>.
All other AWS services will reject deployment into an IPv6-only subnet, so it’s
not possible to add a Lambda function or a Load Balancer into an IPv6-only subnet, for example.</p>

<p>But dual-stack subnets are supported by the major AWS compute services, so IPv6 egress connections are possible from
EC2 instances, containers running on ECS<sup id="fnref:ecsipv6" role="doc-noteref"><a href="#fn:ecsipv6" class="footnote" rel="footnote">4</a></sup> and EKS (both via Fargate and
EC2), and VPC-enabled Lambda functions.</p>

<p>Most other VPC-enabled services only support IPv4 – they simply ignore IPv6 when deployed into a dual-stack subnet; examples include App Runner,
API Gateway, AppSync, and CodeBuild. In a VPC without any public IPv4 connectivity (or PrivateLink), they will not be able to connect
to external resources, including AWS services.</p>

<hr />

<h2 id="accessing-aws-services-via-ipv6">Accessing AWS Services via IPv6</h2>

<p>But even when a VPC resource can initiate connections via IPv6, some public IPv4 egress (or PrivateLink) is still
required for many workloads, as most AWS services do not support requests from IPv6 clients.
Here are some example requirements that <strong>cannot work</strong> otherwise:</p>

<ul>
  <li>Pulling container images from the Elastic Container Registry</li>
  <li>Sending e-mails via SES (neither via API nor via SMTP)</li>
  <li>Sending metrics or X-Ray traces to CloudWatch</li>
  <li>Managing EC2 instances via
<a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/what-is-systems-manager.html">Systems Manager (SSM)</a>,
including <a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html">Session Manager</a>,
<a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/patch-manager.html">Patch Manager</a>, etc.</li>
  <li>Dynamic SSH keys for EC2 instance access via <a href="/2020/08/25/aws-ssh.html">EC2 Instance Connect</a></li>
  <li>Connecting to any (public) API Gateway endpoint</li>
  <li>Using the <a href="/2021/03/16/ecs-exec.html">ECS Exec feature</a> to log into ECS tasks</li>
  <li>Using any AWS service that does not support IPv6 on its endpoints, like SQS, SNS, Eventbridge, Bedrock, and many others</li>
</ul>

<p>This also applies to third-party services without IPv6 support like Github<sup id="fnref:github" role="doc-noteref"><a href="#fn:github" class="footnote" rel="footnote">5</a></sup>.</p>

<p>Many AWS services deploy resources into the customer’s VPC, like RDS or ElastiCache. Connecting to those resources
happens entirely within the VPC, so this works fine without public IPv4 connectivity (using the VPC’s private IPv4 addresses).
That’s also how PrivateLink Interface Endpoints work.</p>

<h3 id="cloudwatch-logs">Cloudwatch Logs</h3>

<p>Many applications need to send log output to Cloudwatch Logs.
This has been supported via IPv6 <a href="https://aws.amazon.com/about-aws/whats-new/2024/02/amazon-cloudwatch-logs-ipv6/">since February 2024</a>,
but not by default – so sending logs to Cloudwatch requires configuration of each application.
For example, for the Cloudwatch Agent (EC2),
this can be specified via the <code class="language-plaintext highlighter-rouge">endpoint_override</code> option in the
<a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Agent-Configuration-File-Details.html">configuration file</a>.</p>

<h3 id="elastic-container-service">Elastic Container Service</h3>

<p>While ECS supports IPv6 for workloads, it often needs access to other AWS services as part of starting the task.</p>

<p>AWS’s Elastic Container Registry does not support IPv6, so ECS tasks cannot pull container images from ECR via IPv6.
<a href="https://www.docker.com/blog/docker-hub-registry-ipv6-support-now-generally-available/">Dockerhub supports IPv6</a>
though.</p>

<p>ECS tasks are commonly configured to send the containers’ log output to Cloudwatch, using the
<code class="language-plaintext highlighter-rouge">awslogs</code> driver in the logging configuration. As mentioned above, extra configuration is required – but <code class="language-plaintext highlighter-rouge">awslogs</code>
does not support endpoint configuration (<a href="https://github.com/aws/containers-roadmap/issues/73">Github issue #73</a>, open since 2018).
Tasks will fail to start unless configured without logging or using a different logs driver. There was a
<a href="https://twitter.com/nathankpeck/status/1719021436923208048">suggested workaround</a>
using the <a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/using_firelens.html">FireLens driver</a> to send logs to Cloudwatch.</p>

<p>ECS supports <a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/specifying-sensitive-data.html">retrieving secret values</a> from either
Secrets Manager or the SSM Parameter Store, but the latter does not support IPv6.</p>

<p>Apropos ECS: The ECS service team has published a pattern:
<a href="https://containersonaws.com/pattern/dual-stack-ipv6-networking-ecs-fargate">Dual-stack IPv6 networking for Amazon ECS and AWS Fargate</a>. It describes
an ECS setup without public IPv4 in great technical detail and has some example code. The issues I mention here are solved with PrivateLink Interface Endpoints
for ECR, Cloudwatch, etc. (for about $160/month). A great lab setup to gain experience with IPv6 on AWS!</p>

<p>I don’t know too much about EKS, but I assume the situation is similar there. AWS offers an
<a href="https://catalog.us-east-1.prod.workshops.aws/workshops/3b06259f-8e17-4f2f-811a-75c9b06a2807/en-US">IPv6 workshop for EKS</a>, by the way.</p>

<hr />

<h2 id="hard-truth">Hard Truth</h2>

<p>Clearly, it’s not feasible to fully avoid public IPv4 for egress on AWS, except for very specific use cases.</p>

<p>If some public IPv4 is required, the general advice for costs is this: A small self-managed NAT Instance is usually the cheapest option by far (in fact, the
required public IPv4 address can cost more than the EC2 instance).
For maximum quality of life, Managed NAT Gateway is the best choice – and for any enterprise environment, this is a no-brainer, despite its price tag.</p>

<p>But there are grey areas for small setups:
Using public IPv4 addresses directly is cheaper than Managed NAT Gateway until around ten IPv4 addresses (supported for EC2 instances and ECS Fargate),
and using PrivateLink is cheaper than Managed NAT Gateway until five Interface Endpoints. This is <em>per Availability Zone</em> for both, if AZ redundancy is required.
In large environments, it’s common to use Resource Sharing and/or Transit Gateway to
<a href="https://docs.aws.amazon.com/whitepapers/latest/building-scalable-secure-multi-vpc-network-infrastructure/centralized-egress-to-internet.html">centralize egress</a> –
but this is out of scope for this post.</p>

<p>So if some public IPv4 is required anyway, it may seem pointless to spend time on IPv6 for egress at all, especially as it adds complexity.
I recommend doing it anyway, as it’s important to gain
operational experience with IPv6 – it’s coming slowly, but it’s coming. And every bit of egress traffic that doesn’t go through a NAT Gateway decreases costs.</p>

<hr />

<h2 id="aws-service-endpoints-and-client-applications-aws-cli-and-sdk">AWS Service Endpoints and Client Applications (AWS CLI and SDK)</h2>

<p>The remainder of this post shows how to make the AWS SDK use IPv6, so it’s only relevant when running entirely without public IPv4 connectivity (and PrivateLink).</p>

<h3 id="on-aws-service-endpoints">On AWS Service Endpoints</h3>

<p>When an AWS application – anything using the AWS SDK, including the AWS CLI – needs to send requests to an AWS service,
it must know which <a href="https://docs.aws.amazon.com/general/latest/gr/rande.html">service endpoint</a> to connect to.</p>

<p>Endpoints are usually region-specific DNS names, like <code class="language-plaintext highlighter-rouge">logs.eu-central-1.amazonaws.com</code> for Cloudwatch Logs in Frankfurt.
Some services are global<sup id="fnref:partitions" role="doc-noteref"><a href="#fn:partitions" class="footnote" rel="footnote">6</a></sup> instead, so they don’t contain a region name, like <code class="language-plaintext highlighter-rouge">iam.amazonaws.com</code> or <code class="language-plaintext highlighter-rouge">cloudfront.amazonaws.com</code>.</p>

<p>The SDK doesn’t actually care much about IPv4 vs. IPv6; it just connects to the service endpoint using the standard
behavior of the operating system (usually: try IPv6 first, then try IPv4).</p>

<p>For the few services that support IPv6 endpoints, they usually do so only on <em>separate</em> dual-stack endpoints.
This is an important and surprising detail – virtually everyone in the world implements IPv6 on websites and APIs by allowing requests via IPv6
and IPv4 on the <em>same</em> endpoint, so IPv6 “just works” when available on both ends of the connection.</p>

<p>But on AWS, IPv6 is usually not supported on the default service endpoint. For example,
in Ireland (eu-west-1), EC2 supports IPv6 clients, but on a separate endpoint:</p>
<ul>
  <li>default: <code class="language-plaintext highlighter-rouge">ec2.eu-west-1.amazonaws.com</code> (IPv4 only)</li>
  <li>dual-stack: <code class="language-plaintext highlighter-rouge">ec2.eu-west-1.api.aws</code> (IPv4 and IPv6)</li>
</ul>

<p>Some AWS services need to discover endpoints dynamically – like Timestream, where the SDK needs to connect to the correct
<a href="https://docs.aws.amazon.com/timestream/latest/developerguide/Using-API.endpoint-discovery.how-it-works.html"><em>cell</em></a> that handles the requested
Timestream database. This mechanism isn’t aware of different endpoint flavors, so it could not support separate dual-stack
endpoints, as far as I can tell.</p>

<p>Unfortunately, the AWS SDK will never attempt to use a service’s dual-stack endpoint by default. It must be configured to do so, or it will
never use IPv6 for most services.</p>

<h3 id="service-endpoint-configuration">Service Endpoint Configuration</h3>

<h4 id="using-dual-stack-endpoints">Using Dual-Stack Endpoints</h4>

<p>By default, when no endpoint has been given explicitly, the SDK will look up the service endpoint using an internal
ruleset.
This supports a configuration option to make it
<a href="https://docs.aws.amazon.com/sdkref/latest/guide/feature-endpoints.html">select a dual-stack endpoint</a>.</p>

<p>Using this option is preferred over explicitly specifying an endpoint, if possible.</p>

<p>The effect can be verified easily by using AWS CLI with <code class="language-plaintext highlighter-rouge">--debug</code>, as it will log the result of the rule-based lookup.</p>

<p>Standard behavior:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ aws --debug ec2 describe-instances 2&gt;&amp;1 | grep "Endpoint provider result"
[...] Endpoint provider result: https://ec2.eu-west-1.amazonaws.com
</code></pre></div></div>
<p>Requesting the dual-stack endpoint via environment variable:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ export AWS_USE_DUALSTACK_ENDPOINT=true
$ aws --debug ec2 describe-instances 2&gt;&amp;1 | grep "Endpoint provider result"
[...] Endpoint provider result: https://ec2.eu-west-1.api.aws
</code></pre></div></div>

<p>Configuring this via environment variables also works for other SDK applications, not just the CLI.
It can also be enabled in code when initializing the SDK client. Python example:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>import boto3
from botocore.config import Config

ec2 = boto3.client(
	service_name = 'ec2',
	config = Config(
		use_dualstack_endpoint = True,
	),
)
</code></pre></div></div>

<p>But if the SDK is configured to select a dual-stack endpoint, it will generate a dual-stack endpoint name
even if that is not supported for the service.
There is <a href="https://github.com/boto/botocore/issues/3060">no fallback to the default endpoint</a>. Requesting a dual-stack endpoint for EC2
in Frankfurt (eu-central-1), for example, would abort with a connection error because <code class="language-plaintext highlighter-rouge">ec2.eu-central-1.api.aws</code> does not exist.
EC2 supports dual-stack endpoints only in some regions.</p>

<p>Therefore, it’s important to carefully check which service supports which endpoints in which regions. I have built a map of <a href="https://awsipv6.neveragain.de">AWS Service Endpoints by Region and IPv6 Support</a> to navigate the chaos.</p>

<h4 id="explicitly-specifying-endpoints">Explicitly Specifying Endpoints</h4>

<p>A service endpoint can also be specified explicitly. Python example:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>import boto3

ec2 = boto3.client(
        service_name = "ec2",
        endpoint_url = "https://ec2.eu-west-1.api.aws",
)
</code></pre></div></div>

<p>AWS CLI example:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ aws --endpoint https://ec2.eu-west-1.api.aws ec2 describe-instances
</code></pre></div></div>

<p>Or using environment variables:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ export AWS_ENDPOINT_URL=https://ec2.eu-west-1.api.aws
$ aws ec2 describe-instances
</code></pre></div></div>

<p>Additionally, it’s possible to configure service-specific endpoints like this:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ export AWS_ENDPOINT_URL_LAMBDA=https://lambda.eu-central-1.api.aws
</code></pre></div></div>
<p>This makes it easier to configure an application that needs to access several services, without modifying the code.</p>

<p>Both endpoint configuration options are also supported via the configuration files.
The full documentation for specifying endpoints is <a href="https://docs.aws.amazon.com/sdkref/latest/guide/feature-ss-endpoints.html">here</a>, but
some SDK languages lack support for this (e.g. Java and Rust).</p>

<h3 id="ipv6-enabled-default-endpoints">IPv6-Enabled Default Endpoints</h3>

<p>A few services (like Secrets Manager) actually support IPv6 on their default service endpoint.</p>

<p>It’s indeed possible to set up an IPv6-only subnet, launch a new IPv6-only EC2 instance, and run this command:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ aws secretsmanager list-secrets
</code></pre></div></div>
<p>This “just works”, without any configuration and in all supported regions. As it should be.</p>

<p>(But if the “use dual-stack endpoint” option is enabled, this command will assume a wrong service endpoint and fail.)</p>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>The basic building blocks are there. AWS has done all the hard work –
the IPv6 support in VPC, EC2, Lambda, and ECS/EKS is good.</p>

<p>But the obstructive SDK behavior and the frugal IPv6 support of service endpoints make IPv6 complex and error-prone to implement on AWS.
And that’s in addition to all the <a href="https://twitter.com/apparentorder/status/1730866951340077540">little things</a> that crop up. It all
feels like it has never been seriously used without public IPv4 outside a lab environment.</p>

<p>It’s pretty clear that IPv6 on AWS will not see widespread adoption this way. Just ignoring IPv6 for egress is so much easier than
running without public IPv4 addresses. This way we will never escape IPv4.
So I can only hope that AWS can rally service teams to add IPv6 support to their endpoints. Preferably to the default endpoints,
as the rest of the world does – this would alleviate the SDK issues significantly.</p>

<p>In other words – referring to
<a href="https://aws.amazon.com/blogs/aws/new-aws-public-ipv4-address-charge-public-ip-insights/">Jeff Barr’s initial blog post</a>
about the IPv4 charges –, I’d like to encourage AWS to
think about accelerating adoption of IPv6 as a modernization and conservation measure.</p>

<hr />

<p>Other links to this article: <a href="https://twitter.com/apparentorder/status/1792630493457813669">Twitter</a></p>

<hr />

<p>This is part of a blog series:</p>
<ul>
  <li>Part One: <a href="/2023/09/21/aws-cannot-escape-ipv4.html">Cannot Escape IPv4</a></li>
  <li>Part Two: <a href="/2023/10/24/aws-ipv6-ingress.html">Ingress Traffic: Avoiding Public IPv4 Address Charges</a></li>
  <li>Part Three: Egress Traffic and Using AWS Services via IPv6 (this part)</li>
</ul>

<hr />

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:fcknat" role="doc-endnote">
      <p>There are several third-party projects that help with NAT Instances, for example <a href="https://fck-nat.dev/stable/">fck-nat</a> (which will
soon <a href="https://github.com/AndrewGuenther/fck-nat/issues/41">implement NAT64 support</a> as well) <a href="#fnref:fcknat" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:gwendpoint" role="doc-endnote">
      <p>There are exactly two AWS services that also provide
<a href="https://docs.aws.amazon.com/vpc/latest/privatelink/gateway-endpoints.html"><em>Gateway Endpoints</em></a> to access them from a VPC – DynamoDB and S3. Unlike
Interface Endponts, these endpoints are route-based and don’t cost a thing. <a href="#fnref:gwendpoint" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:nat" role="doc-endnote">
      <p>Nowadays, there are NAT solutions for IPv6, but I’m <em>really</em> happy that AWS has implemented IPv6 as God has intended it – without
any address translation. NAT was bolted onto IPv4 in despair. It breaks things, it wastes a tremendous amount of resources, it is a
pain to manage and it cripples innovation. Not to mention the salty pricing of AWS Managed NAT Gateway, which includes traffic-based
charges. I hear that other cloud providers implement IPv6 with NAT. <a href="#fnref:nat" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:ecsipv6" role="doc-endnote">
      <p>For some accounts, ECS dual-stack support might be disabled: “If you had a running task using the awsvpc network mode
in an IPv6 enabled subnet between the dates of October 1, 2020 and November 2, 2020, the default dualStackIPv6 account setting in
the Region that the task was running in is disabled. If that condition isn’t met, the default dualStackIPv6 setting in the Region is
enabled.”
(<a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-account-settings.html">ECS Account Settings</a>) <a href="#fnref:ecsipv6" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:github" role="doc-endnote">
      <p>Apparently, Github was already flexing IPv6 support for Github <a href="https://twitter.com/AS36459/status/1582728252199964672">back in 2022</a>,
and was still busy with preparations in January 2024 per <a href="https://www.githubstatus.com/incidents/5y8b8lsqbbyq">this incident report</a>. <a href="#fnref:github" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:partitions" role="doc-endnote">
      <p>Actually, the services are “global” only within their own
<a href="https://docs.aws.amazon.com/whitepapers/latest/aws-fault-isolation-boundaries/partitions.html"><em>AWS partition</em></a>; hence they
are sometimes referred to as <em>partition-wide endpoints</em>. For example, there is <code class="language-plaintext highlighter-rouge">iam.amazonaws.com</code> for the standard commercial
<code class="language-plaintext highlighter-rouge">aws</code> partition, but the <code class="language-plaintext highlighter-rouge">aws-us-gov</code> partition for the
<a href="https://aws.amazon.com/govcloud-us">AWS GovCloud</a> uses <code class="language-plaintext highlighter-rouge">iam.us-gov.amazonaws.com</code>. <a href="#fnref:partitions" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[An overview of the available options for IPv6 egress traffic on AWS and a quick intro to using AWS services via IPv6]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://tty.neveragain.de/assets/cyberroad-400.jpeg" /><media:content medium="image" url="https://tty.neveragain.de/assets/cyberroad-400.jpeg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">AWS: On IAM Policies for the Systems Manager Agent</title><link href="https://tty.neveragain.de/2023/10/31/ssm-iam.html" rel="alternate" type="text/html" title="AWS: On IAM Policies for the Systems Manager Agent" /><published>2023-10-31T00:00:00+01:00</published><updated>2023-10-31T00:00:00+01:00</updated><id>https://tty.neveragain.de/2023/10/31/ssm-iam</id><content type="html" xml:base="https://tty.neveragain.de/2023/10/31/ssm-iam.html"><![CDATA[<p><img src="https://tty.neveragain.de/assets/sheep-fence-400x324.jpeg" alt="sheep jumping over a fence" style="float: left; margin-right: 1em" width="400" height="324" /></p>

<h2 id="thats-not-supposed-to-work">That’s Not Supposed to Work!</h2>

<p>Every now and then, I run a command that I <em>expect</em> to fail; usually to verify the system’s behavior before and after a change. So I was querying a secret from the
<a href="https://docs.aws.amazon.com/de_de/systems-manager/latest/userguide/systems-manager-parameter-store.html">Parameter Store</a> on some throwaway EC2 instance with no special permissions – and it worked. Oops!</p>

<h2 id="iam-policies-for-the-ssm-agent">IAM Policies for the SSM Agent</h2>

<p>The <a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent.html">Systems Manager Agent</a> is the recommended way to manage EC2 instances,
e.g. for automated patching and as an <a href="https://tty.neveragain.de/2020/08/25/aws-ssh.html">alternative to SSH</a>.</p>

<p>Over the years, different IAM Policies were recommended for the SSM Agent:</p>

<p>Since 2019, the recommended policy was <a href="https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AmazonSSMManagedInstanceCore.html"><code class="language-plaintext highlighter-rouge">AmazonSSMManagedInstanceCore</code></a> (setup instructions <a href="https://web.archive.org/web/20230129201610/https://docs.aws.amazon.com/systems-manager/latest/userguide/setup-instance-profile.html">archive link</a>). Unfortunately, this policy allows to read all parameters from the Parameter Store (<code class="language-plaintext highlighter-rouge">ssm:GetParameter</code> for <code class="language-plaintext highlighter-rouge">*</code>).</p>

<p>This sounds rather harmless
at first, but Parameter Store can also store secrets (type <code class="language-plaintext highlighter-rouge">SecureString</code>). It’s pretty common to use Parameter Store for secrets, as the <a href="https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html">Secrets Manager</a>
service charges $0.40 for every
single secret (and its additional features are usually not needed). So my EC2 instance was able to access all secrets in the AWS account, just because it was running the SSM Agent.</p>

<p>The previous policy was even worse:
<a href="https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AmazonEC2RoleforSSM.html"><code class="language-plaintext highlighter-rouge">AmazonEC2RoleforSSM</code></a>
was recommended until 2019. To this day, it includes full read and write access to all S3 buckets in the account (<code class="language-plaintext highlighter-rouge">s3:PutObject</code> and
<code class="language-plaintext highlighter-rouge">s3:GetObject</code> for <code class="language-plaintext highlighter-rouge">*</code>) – and the mentioned permissions to Parameter Store as well. The role has not been deprecated yet, but
the policy’s documentation says this is supposed to happen “soon” and suggests to use <code class="language-plaintext highlighter-rouge">AmazonSSMManagedInstanceCore</code> instead.</p>

<p>Since fall of 2022, there is a new policy <a href="https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AmazonSSMManagedEC2InstanceDefaultPolicy.html"><code class="language-plaintext highlighter-rouge">AmazonSSMManagedEC2InstanceDefaultPolicy</code></a>. The only difference to the <code class="language-plaintext highlighter-rouge">AmazonSSMManagedInstanceCore</code> role is that it
doesn’t include permissions to the SSM Parameter Store. This policy is meant to be used with the new <a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/managed-instances-default-host-management.html">Default Host Management Configuration</a> (a great overview of how this works under the
hood can be found on <a href="https://awsteele.com/blog/2023/02/20/a-role-for-all-your-ec2-instances.html">Aidan Steele’s blog</a>).</p>

<h2 id="impact">Impact</h2>

<p>According to a recent scientific study<sup id="fnref:poll" role="doc-noteref"><a href="#fn:poll" class="footnote" rel="footnote">1</a></sup>, almost two in three customers use one of the older policies – likely unaware
that it might allow unintended access to all secrets (and all S3 buckets, in case of the oldest policy). That’s not too surprising, as <code class="language-plaintext highlighter-rouge">AmazonSSMManagedInstanceCore</code> still is recommended as part of the <a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/create-iam-roles-for-cloudwatch-agent.html#create-iam-roles-for-cloudwatch-agent-roles">Cloudwatch Agent setup</a> and for the SSM Agent setup <a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/setup-instance-permissions.html#instance-profile-add-permissions">without DHMC</a>.</p>

<h2 id="alternatives">Alternatives</h2>

<p>The way forward is to adopt DHMC. If that’s not an option, <a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html">permission boundaries</a> can help, or simply replacing the policy with the newer <code class="language-plaintext highlighter-rouge">AmazonSSMManagedEC2InstanceDefaultPolicy</code> or a custom policy.</p>

<p>Another option is using a custom KMS key to <a href="https://docs.aws.amazon.com/kms/latest/developerguide/services-parameter-store.html">encrypt the parameters</a>. In
that case, the parameter could still be read, but decrypting its value would fail unless the EC2 instance has permission to use the key.</p>

<h2 id="conclusion">Conclusion</h2>

<p>The most flexible and – usually – most secure option is to write a custom IAM policy for everything. Most customers
use the provided Managed IAM Policies instead, and the AWS service documentations usually recommend doing so. Given
the amount of services and the complexity of IAM interactions, I’d even say it’s not feasible to write and maintain custom
policies for every use-case, except for organizations with dedicated security teams.</p>

<p>Still, Managed Policies should not be trusted blindly. Those policies can change at any time<sup id="fnref:mamip" role="doc-noteref"><a href="#fn:mamip" class="footnote" rel="footnote">2</a></sup>, and it’s
always easy to miss something – like I did in this case.</p>

<hr />

<p>Other links to this article: <a href="https://twitter.com/apparentorder/status/1719437011394662627">Twitter</a></p>

<p><em>Update 2023-11-01</em> – updated to reflect that the issue only applies to parameters encrypted with the <em>default</em> KMS key (thanks to Aidan Steele for keeping
me on my toes!)</p>

<hr />

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:poll" role="doc-endnote">
      <p>Okay, actually an absolutely-not-statistically-significant <a href="https://twitter.com/apparentorder/status/1718641659922075705">poll on Twitter</a> with
     just 30 replies. <a href="#fnref:poll" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:mamip" role="doc-endnote">
      <p><a href="https://github.com/zoph-io/MAMIP">MAMIP</a> monitors those policies for changes and sends notifications via Twitter, Mastodon, Github and RSS feed. <a href="#fnref:mamip" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[The AmazonSSMManagedInstanceCore IAM policy, until recently the recommended policy for the Systems Manager Agent on EC2 instances, allows retrieval of secrets stored in the Parameter Store]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://tty.neveragain.de/assets/sheep-fence-400x324.jpeg" /><media:content medium="image" url="https://tty.neveragain.de/assets/sheep-fence-400x324.jpeg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">AWS: Ingress Traffic: Avoiding Public IPv4 Address Charges</title><link href="https://tty.neveragain.de/2023/10/24/aws-ipv6-ingress.html" rel="alternate" type="text/html" title="AWS: Ingress Traffic: Avoiding Public IPv4 Address Charges" /><published>2023-10-24T00:00:00+02:00</published><updated>2023-10-24T00:00:00+02:00</updated><id>https://tty.neveragain.de/2023/10/24/aws-ipv6-ingress</id><content type="html" xml:base="https://tty.neveragain.de/2023/10/24/aws-ipv6-ingress.html"><![CDATA[<hr />

<p><img src="https://tty.neveragain.de/assets/ingress.jpeg" alt="Something that vaguely resembles a gloomy ingress construct" style="float: left; margin-right: 1em" width="400" height="400" /></p>

<h2 id="introduction">Introduction</h2>

<p>AWS will soon start to charge customers for public IPv4 addresses. I recommend reading my <del>rant</del> analysis of the AWS announcement
first: <a href="/2023/09/21/aws-cannot-escape-ipv4.html">Cannot Escape IPv4</a>.</p>

<p>This post will take a quick look at available options specifically for ingress traffic – how to get requests from end users to an
application  built on AWS.</p>

<p>The first goal is providing access to end users on both IPv4 <em>and</em> IPv6. Global IPv6 adoption is around 50% for end users<sup id="fnref:v6stats" role="doc-noteref"><a href="#fn:v6stats" class="footnote" rel="footnote">1</a></sup>,
so it’s imperative to provide the best connectivity. At the same time, it’s still necessary to serve end users that are limited to
IPv4.</p>

<p>The second goal is avoiding the AWS public IPv4 address charges – for reasons outlined in my previous post, I want to show that
there is often no good way to avoid those charges. And because it helps IPv4 address conservation, of course. To be clear, I do
<em>not</em> recommend actually doing this, as these charges will have a very small effect on the AWS bill; it would be silly to change
architecture for a few bucks. For most customers.</p>

<h2 id="dedicated-and-shared-ipv4-addresses">Dedicated and Shared IPv4 Addresses</h2>

<p>The new AWS IPv4 charges only apply to public IPv4 addresses that are <em>dedicated</em>, meaning that they are used exclusively by a
single AWS customer.</p>

<p>For web (HTTP-based) applications, it’s common to use <em>shared</em> IP addresses, meaning many customers can be hosted on the same IP
address (using <a href="https://en.wikipedia.org/wiki/Virtual_hosting">HTTP Virtual Hosting</a> and <a href="https://en.wikipedia.org/wiki/Server_Name_Indication">TLS
SNI</a>). Some AWS services provide centralized ingress using shared IP addresses
– consequently, there are no IPv4 address charges with those services.</p>

<h2 id="ingress-services-in-detail">Ingress Services in Detail</h2>

<p>The following table lists common AWS services used for ingress and their capabilities regarding the two goals.</p>

<table>
  <thead>
    <tr>
      <th>Service</th>
      <th>IPv4 ingress</th>
      <th>IPv6 ingress</th>
      <th>IPv6 to origin / VPC</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Cloudfront (CDN)</td>
      <td>✅</td>
      <td>✅</td>
      <td>❌</td>
    </tr>
    <tr>
      <td>EC2 instances</td>
      <td>❌</td>
      <td>✅</td>
      <td>✅</td>
    </tr>
    <tr>
      <td>ECS (Fargate and EC2)</td>
      <td>❌</td>
      <td>✅</td>
      <td>✅</td>
    </tr>
    <tr>
      <td>Elastic Load Balancing</td>
      <td>❌</td>
      <td>✅</td>
      <td>✅</td>
    </tr>
    <tr>
      <td>Elastic Beanstalk</td>
      <td>❌</td>
      <td>❌</td>
      <td>❌</td>
    </tr>
    <tr>
      <td>App Runner</td>
      <td>✅</td>
      <td>❌</td>
      <td>❌</td>
    </tr>
    <tr>
      <td>API Gateway</td>
      <td>✅</td>
      <td>❌</td>
      <td>❌</td>
    </tr>
    <tr>
      <td>Amplify Hosting</td>
      <td>✅</td>
      <td>❌</td>
      <td>n/a</td>
    </tr>
    <tr>
      <td>AppSync</td>
      <td>✅</td>
      <td>❌</td>
      <td>n/a</td>
    </tr>
    <tr>
      <td>Global Accelerator</td>
      <td>❌</td>
      <td>✅</td>
      <td>✅</td>
    </tr>
    <tr>
      <td>Lambda Function URLs</td>
      <td>✅</td>
      <td>✅</td>
      <td>✅</td>
    </tr>
    <tr>
      <td>S3 Website Hosting<sup id="fnref:s3" role="doc-noteref"><a href="#fn:s3" class="footnote" rel="footnote">2</a></sup></td>
      <td>✅</td>
      <td>❌</td>
      <td>n/a</td>
    </tr>
  </tbody>
</table>

<p>All services support IPv4 ingress; the first column shows whether the service uses a <em>shared</em> IPv4 address (no charge) or a
<em>dedicated</em> IPv4 address (incurs public IPv4 charges).</p>

<p>The last column is included for completeness; it’s not relevant to the two goals.</p>

<h3 id="elastic-load-balancer-and-global-accelerator">Elastic Load Balancer and Global Accelerator</h3>

<p>Probably the most popular choice of ingress is Application Load Balancer, which routes HTTP requests to backend applications. For
other non-HTTP applications, Network Load Balancer is a common choice. Global Accelerator can be used to provide public ingress IPs
for (internal) Load Balancers and EC2 instances.</p>

<p>Fortunately, these AWS services do support IPv6 for ingress. Unfortunately, they do not allow to opt out of IPv4, so even if an
application would only need IPv6, charges for multiple IPv4 addresses will apply. For a Load Balancer in three Availability Zones,
this will be a base price increase of more than 50%.</p>

<p>As Load Balancers and Global Accelerator violate the second goal, we need to look at other AWS services to provide ingress.</p>

<h3 id="api-gateway">API Gateway</h3>

<p>API Gateway supports <a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-vpc-links.html">VPC Links</a> on HTTP APIs.
Together with <a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-http.html">proxy
integrations</a>, this allows
some interesting combinations.</p>

<p>API Gateway can be combined with the Cloud Map service discovery to provide ingress for ECS Services. See this <a href="https://aws.amazon.com/blogs/architecture/field-notes-integrating-http-apis-with-aws-cloud-map-and-amazon-ecs-services/">description on the
AWS blog</a>
(2020) or the <a href="https://containersonaws.com/pattern/api-gateway-fargate-cloudformation">entry on “Containers on AWS”</a> (2023, with SAM /
Cloudformation).</p>

<p>API Gateway can also be the entry point for an <em>internal</em> Load Balancer which doesn’t use public IPv4 addresses.</p>

<p>API Gateway has some noteworthy limitations, like a maximum request duration of 30 seconds and no support for unencrypted HTTP. And
no IPv6 support.</p>

<h3 id="elastic-beanstalk">Elastic Beanstalk</h3>

<p>Elastic Beanstalk orchestrates Elastic Load Balancer and compute resources. While the underlying resources do support IPv6, Elastic
Beanstalk offers no way of configuring it. There are Github issues on the EB Roadmap that have been open and unanswered since 2021
for both <a href="https://github.com/aws/elastic-beanstalk-roadmap/issues/167">ingress</a> and <a href="https://github.com/aws/elastic-beanstalk-roadmap/issues/208">VPC
connectivity</a>.</p>

<p>Elastic Beanstalk seemed somewhat abandoned for years, and this shows in its IPv6 support as well.</p>

<h3 id="others">Others</h3>

<p>For simple container-based applications, migrating to <a href="https://tty.neveragain.de/2021/06/18/app-runner-deep-dive.html">App Runner</a>
could be an interesting option. It does not support IPv6 though, and has its own set of limitations.</p>

<p>EC2 instances can run in IPv6-only subnets, so if no additional AWS services are required, this would be a good option.</p>

<p>Lambda Function URLs are a nice surprise, as they support IPv6 not only for ingress, but now also for egress (to VPC and to
internet) – a feature added <a href="https://aws.amazon.com/about-aws/whats-new/2023/10/aws-lambda-ipv6-outbound-connections-vpc/">just a few days
ago</a>. Many existing applications are
not easily migrated to Lambda though<sup id="fnref:lwa" role="doc-noteref"><a href="#fn:lwa" class="footnote" rel="footnote">3</a></sup>.</p>

<h3 id="content-delivery-networks">Content Delivery Networks</h3>

<p>When an AWS service has missing IPv6 support, this can usually be fixed by adding a Content Delivery Network (CDN) with support for
IPv6 at the edge, like Cloudfront; many applications already use Cloudfront anyway. Cloudfront can serve IPv4 and IPv6 to end users,
while talking to origin services over IPv4.</p>

<p>Cloudfront does not support IPv6 to origins though, so it would not help with IPv6-only EC2 instances. CDN competitor Cloudflare
seized the opportunity to <del>make fun of AWS</del> help AWS customers out by <a href="https://blog.cloudflare.com/amazon-2bn-ipv4-tax-how-avoid-paying/">offering a
discount</a><sup id="fnref:cloudflare" role="doc-noteref"><a href="#fn:cloudflare" class="footnote" rel="footnote">4</a></sup> for using IPv6 origins on AWS. This allows
to serve IPv4 and IPv6 end users while running IPv6-only EC2 instances. In many cases, a CDN can even be used for simple load
balancing between EC2 instances.</p>

<p>One big drawback of not using Application Load Balancer in front of EC2 instances is the overhead for installing and managing TLS certificates.</p>

<h2 id="conclusion">Conclusion</h2>

<p>When avoiding Elastic Load Balancers, which incur IPv4 address charges, the support for both IP protocols can usually be achieved by
using a CDN. On the other hand, most workarounds to avoid Elastic Load Balancers are neither technically nor financially good ideas.
If anything, this serves to show the weird state of IPv6 support on AWS.</p>

<p>I would love to see Elastic Load Balancing add the option to run without IPv4 addresses, combined with Cloudfront support for IPv6
origins. This combination would be a relatively easy fix for many setups, and it would be a big step towards making IPv6-only setups
feasible.</p>

<p>But until AWS IPv6 support improves, the only sensible option for customers using Elastic Load Balancers is to just accept the
upcoming IPv4 charges.</p>

<hr />

<p>This is part of a blog series:</p>
<ul>
  <li>Part One: <a href="/2023/09/21/aws-cannot-escape-ipv4.html">Cannot Escape IPv4</a></li>
  <li>Part Two: Ingress Traffic: Avoiding Public IPv4 Address Charges (this part)</li>
  <li>Part Three: <a href="/2024/05/20/aws-ipv6-egress.html">Egress Traffic and Using AWS Services via IPv6</a></li>
</ul>

<hr />

<p>Other links to this article: <a href="https://twitter.com/apparentorder/status/1716767808556564609">Twitter</a></p>

<p><em>Addendum 2024-05-20 (1)</em> – As of November 2023,
  <a href="https://aws.amazon.com/about-aws/whats-new/2023/11/aws-app-runner-supports-ipv6-public-inbound-traffic/">App Runner supports IPv6 ingress</a>,
  making it a solid choice for quick and easy IPv6-enabled ingress without extra IPv4 address charges.</p>

<p><em>Addendum 2024-05-20 (2)</em> – As of May 2024,
  <a href="https://aws.amazon.com/about-aws/whats-new/2024/05/application-load-balancer-ipv6-internet-clients/">Application Load Balancer supports IPv6-<em>only</em> ingress</a>,
  allowing to run ALBs without public IPv4 addresses. This feature was on my conclusion’s wish list! Now if Cloudfrount adds
  IPv6 origin support as well, this would be great.</p>

<p><em>Update 2023-10-25</em> – added link to “Containers on AWS” for API Gateway / Cloud Map / ECS</p>

<hr />

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:v6stats" role="doc-endnote">
      <p><a href="https://www.google.com/intl/en/ipv6/statistics.html#tab=per-country-ipv6-adoption">Google’s IPv6 statistics</a>
        provide a good approximation to end user IPv6 adoption; many countries are around or above 50%, some countries
        have over 70%. <a href="#fnref:v6stats" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:s3" role="doc-endnote">
      <p>S3 does support <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/ipv6-access.html">dual-stack endpoints</a>,
    but those do not support website hosting. <a href="#fnref:s3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:lwa" role="doc-endnote">
      <p>Special mention: The <a href="https://github.com/awslabs/aws-lambda-web-adapter">Lambda Web Adapter</a> (partly inspired by my <a href="https://github.com/apparentorder/reweb">re:Web</a> project) can enable web applications to run on Lambda instead of classic EC2- or container-based compute. <a href="#fnref:lwa" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:cloudflare" role="doc-endnote">
      <p>Apparently, this marketing gag was inspired by <a href="https://twitter.com/apparentorder/status/1706676350478684184">a joke I made</a>. <a href="#fnref:cloudflare" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[An overview of the available options for ingress traffic on AWS, focused on avoiding the new public IPv4 address charges and providing IPv6 to end users]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://tty.neveragain.de/assets/ingress.jpeg" /><media:content medium="image" url="https://tty.neveragain.de/assets/ingress.jpeg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">AWS: Cannot Escape IPv4</title><link href="https://tty.neveragain.de/2023/09/21/aws-cannot-escape-ipv4.html" rel="alternate" type="text/html" title="AWS: Cannot Escape IPv4" /><published>2023-09-21T00:00:00+02:00</published><updated>2023-09-21T00:00:00+02:00</updated><id>https://tty.neveragain.de/2023/09/21/aws-cannot-escape-ipv4</id><content type="html" xml:base="https://tty.neveragain.de/2023/09/21/aws-cannot-escape-ipv4.html"><![CDATA[<hr />

<p><img src="https://tty.neveragain.de/assets/walking-alone.jpeg" alt="trying to get home" style="float: left; margin-right: 1em" width="400" height="400" /></p>

<h2 id="introduction">Introduction</h2>

<p>AWS has announced that they
<a href="https://aws.amazon.com/blogs/aws/new-aws-public-ipv4-address-charge-public-ip-insights/">will charge for public IPv4 addresses</a>
soon. Surprisingly, most reactions to this announcement were welcoming, as it’s believed to bring
a big push for IPv6 adoption.</p>

<p>Their blog had this to say:</p>

<blockquote>
  <p>This change … is also intended to encourage you to be a bit more frugal with your use of public IPv4 addresses and to think about accelerating your adoption of IPv6 as a modernization and conservation measure.</p>
</blockquote>

<p>I will explain why this charge will not affect IPv6 adoption in any meaningful way.</p>

<p>I will also show that it is pretty bold to use IPv6 in trying to soften the blow of this charge: Their own IPv6 support has
<em>huge</em> gaps. They make it impossible for customers to move on from IPv4.</p>

<p>But first, a quick summary, and some costly anti-patterns to watch out for.</p>

<h2 id="whats-going-to-change">What’s Going to Change</h2>

<p>Every <em>dedicated</em> public IPv4 address<sup id="fnref:byoip" role="doc-noteref"><a href="#fn:byoip" class="footnote" rel="footnote">1</a></sup> will be charged with $0.005 per hour (almost $4 per month /
more than $40 per year), starting February 2024.</p>

<p>Some examples of affected services:</p>
<ul>
  <li>Elastic Load Balancers (one address per availability zone, <em>at least</em> two – and IPv4 cannot be turned off)</li>
  <li>EC2 instances &amp; Elastic IPs</li>
  <li>ECS Fargate<sup id="fnref:awsvpc" role="doc-noteref"><a href="#fn:awsvpc" class="footnote" rel="footnote">2</a></sup> tasks configured with public IPs</li>
  <li>Global Accelerator IPs</li>
  <li>Site-to-site VPN IPs</li>
  <li>RDS (nine in ten customers have public IP on RDS by accident)</li>
  <li>Managed NAT Gateways</li>
</ul>

<p>So for a public Elastic Load Balancer configured in three availability zones (as recommended),
this is a base price increase of over 50%.</p>

<p>For Managed NAT Gateway, this is a base price increase of ~10%. I didn’t think those could become more expensive,
but here we are. By the way, this is a good time to remember that a NAT Gateway does <em>not</em> provide zonal redundancy –
a NAT Gateway in every availability zone is required when outbound connections are critical.</p>

<p>All <em>shared</em> IPv4 addresses, like those used for Cloudfront distributions or API Gateway endpoints, are <em>not</em> affected. Public IPv4
addresses used by non-VPC Lambda functions aren’t affected either.</p>

<h2 id="wasteful-patterns">Wasteful Patterns</h2>

<p>I have observed two patterns that waste quite a lot of addresses and are heavily impacted by the upcoming charges.</p>

<p>The first pattern is having multiple Load Balancers (per VPC); this is often the result of using several
readily available Cloudformation templates / Terraform modules, or somehow using Kubernetes ingress controllers that create
a Load Balancer <em>for every service</em>. This is fixed by not doing that! A single Load Balancer can handle many URLs
and services.</p>

<p>The second pattern is intentionally configuring public IPv4 on EC2 instances and Fargate tasks just to avoid Managed NAT Gateway
(commendable until now!). Ironically, this will still be cheaper until hitting around ten public IPv4 addresses per availability zone.</p>

<h2 id="how-to-check">How to Check</h2>

<p>The usage data for public IPv4 addresses is already being accounted, so it’s easy to check.
Usage is currently listed with $0 but will start to cost $0.005 per hour starting in February 2024.</p>

<ul>
  <li>In <em>Billing Dashboard</em> &gt; <em>Bills</em>, usage is displayed as <code class="language-plaintext highlighter-rouge">PublicIPv4:InUseAddress</code>
in the <em>Virtual Private Cloud</em> section</li>
  <li>In <em>Cost Explorer</em>, it’s easy to see usage hours by filtering to only include <code class="language-plaintext highlighter-rouge">PublicIPv4:InUseAddress</code>;
this is nice because Cost Explorer shows usage per day/hour and it’s easy to group e.g. by member account</li>
  <li><em>VPC</em> &gt; <em>VPC IP Address Manager</em> (at the very bottom) &gt; <em>Public IP insights</em> is a nice way to get an overview of
public IPv4 address usage; this part of IPAM is free, but it shows data only per account and region, and it’s
incomplete data – it doesn’t capture transient address usage.</li>
</ul>

<h2 id="accelerating-your-ipv6-adoption">Accelerating Your IPv6 Adoption</h2>

<p>So it does sound pretty obvious: IPv4 addresses will soon cost a few bucks, while IPv6 addresses are free of charge. Even better:
There is no concept of private addresses in IPv6, which means farewell to the Managed NAT Gateway and its magnificent pricing.
It seems like the perfect time to finally adopt IPv6.</p>

<p>There are two approaches to adopting IPv6. Either by going dual-stack, which means configuring IPv4 <em>and</em> IPv6 addresses on all systems.
Or by going IPv6-only internally, with IPv4 connectivity only where facing the general public – for example at the very edge, on the CDN,
which takes care of serving IPv4 clients.</p>

<p>Running IPv6-only is the optimal solution for a future-proof network. Even the U.S. Government, of all places,
recognizes this. A <a href="https://www.whitehouse.gov/wp-content/uploads/2020/11/M-21-07.pdf">memorandum from the Executive Office of the President</a> states:</p>

<blockquote>
  <p>In recent years it has become clear that [running dual-stack] is overly complex to maintain and unnecessary. As a result, standards bodies and leading technology companies began migrating toward IPv6-only deployments, thereby eliminating complexity, operational cost, and threat vectors associated with operating two network protocols.</p>
</blockquote>

<p>That memorandum was issued three years ago.</p>

<p>The underlying IPv6 support is excellent. It’s possible to configure IPv6-only EC2 instances in IPv6-only subnets
and everything works fine, including local DNS, time service, host configuration, security
and all the stuff to be expected in an IPv6-enabled network. Components like VPN, Transit Gateway,
Direct Connect and Network Firewall also support IPv6.</p>

<h2 id="cannot-escape-ipv4">Cannot Escape IPv4<sup id="fnref:title" role="doc-noteref"><a href="#fn:title" class="footnote" rel="footnote">3</a></sup></h2>

<p>The problem is actually using all the (other) Amazon Web Services.</p>

<p>It’s practically impossible to run IPv6-only. Trying to configure core services like API Gateway, Lambda,
ECS or App Runner into IPv6-only subnets makes them either sternly refuse those subnets or throw comically sad error messages like
<code class="language-plaintext highlighter-rouge">Not enough IP space available</code> (Elastic Load Balancer).</p>

<p>And it gets worse: Not even running dual-stack internally helps. Most services, when configured into a dual-stack
subnet, happily ignore that the subnet has IPv6 configured; they can connect to IPv4 targets only. This
<a href="https://twitter.com/tim_nolet/status/1696206569090789416">actually hurts customers</a>.</p>

<p>Almost no service API can be used from a VPC without public IPv4 addresses<sup id="fnref:privatelink" role="doc-noteref"><a href="#fn:privatelink" class="footnote" rel="footnote">4</a></sup>.
Running an innocent <code class="language-plaintext highlighter-rouge">aws s3 ls</code> from an EC2 instance will fail. Using Systems Manager – the recommended way to
connect to and manage an instance – does not work. Accessing SQS from an ECS Task is impossible. And so on.
This is because <a href="https://awsipv6.neveragain.de">more than 90% of all service API endpoints do not support IPv6</a>.
Other endpoints like SES SMTP or ECR repository endpoints<sup id="fnref:dockerhub" role="doc-noteref"><a href="#fn:dockerhub" class="footnote" rel="footnote">5</a></sup> don’t work either. Cloudfront cannot connect
to IPv6 origins.</p>

<p>And then AWS frames this new charge as an “encouragement” to adopt IPv6. <em>This</em> is what ticked me off:
There is no escape, no way to avoid these charges – because they have significantly neglected IPv6 for years.</p>

<h2 id="charges-without-changes">Charges Without Changes</h2>

<p>For most customers, nothing will change. Which is exactly my point.</p>

<p>For customers of any relevant size, this IPv4 charge is a drop in the bucket; they won’t even notice it on their bill.
In my experience so far, this will usually be very well below 1%. Nobody will invest significant engineering effort for that alone.</p>

<p>For many SMBs, hobbyists and startups though, this charge can easily amount to 10-30% of the bill. These customers certainly
would adopt IPv6 to avoid such a price increase.</p>

<p>So that’s where we are: The larger customers do not care about this charge; and those that will feel the impact have
no way to avoid it.</p>

<h2 id="conclusion-so-far">Conclusion So Far</h2>

<p>I think the IPv4 charge by itself is fine – maybe even necessary, given the IPv4 acquisition costs lately.</p>

<p>I would like AWS to be a role model in IPv6 adoption, like they are in many other areas.
If their service landscape had mature IPv6 support, many customers could indeed migrate to IPv6-only environments.
Larger customers still would not care – but it would create a lot of
first movers within the community, setting examples to follow.</p>

<p>But IPv6 is clearly not a first-class citizen today.</p>

<p>Given those circumstances, this charge will do nothing to drive IPv6 adoption. It will stiffle innovation and make it
<a href="https://www.lastweekinaws.com/blog/aws-has-a-moral-responsibility-to-fix-the-free-tier/">even less</a> attractive to have a
personal account – for example, for learning, to study for certifications (to invest in AWS as a career path) or to
support open-source work. AWS talks big about democratizing cloud; this isn’t it.</p>

<hr />

<p>This is part of a blog series:</p>
<ul>
  <li>Part One: Cannot Escape IPv4 (this part)</li>
  <li>Part Two: <a href="/2023/10/24/aws-ipv6-ingress.html">Ingress Traffic: Avoiding Public IPv4 Address Charges</a></li>
  <li>Part Three: <a href="/2024/05/20/aws-ipv6-egress.html">Egress Traffic and Using AWS Services via IPv6</a></li>
</ul>

<hr />

<h2 id="links">Links</h2>

<ul>
  <li><a href="https://d1.awsstatic.com/architecture-diagrams/ArchitectureDiagrams/IPv6-reference-architectures-for-AWS-and-hybrid-networks-ra.pdf">Dual Stack and IPv6-only Amazon VPC Reference Architectures</a> (PDF)</li>
  <li>Workshop: <a href="https://catalog.workshops.aws/ipv6-on-aws/en-US">Get hands-on with IPv6</a></li>
  <li>Blog
    <ul>
      <li><a href="https://aws.amazon.com/blogs/aws/new-aws-public-ipv4-address-charge-public-ip-insights/">Public IPv4 Address Charge + Public IP Insights</a></li>
      <li><a href="https://aws.amazon.com/blogs/networking-and-content-delivery/identify-and-optimize-public-ipv4-address-usage-on-aws/">Identify and optimize public IPv4 address usage</a></li>
      <li>Dual-stack IPv6 architectures and hybrid networks
<a href="https://aws.amazon.com/blogs/networking-and-content-delivery/dual-stack-ipv6-architectures-for-aws-and-hybrid-networks/">Part 1</a>
and <a href="https://aws.amazon.com/blogs/networking-and-content-delivery/dual-stack-architectures-for-aws-and-hybrid-networks-part-2/">Part 2</a></li>
    </ul>
  </li>
  <li>Whitepapers
    <ul>
      <li><a href="https://docs.aws.amazon.com/whitepapers/latest/ipv6-on-aws/designing-an-ipv6-aws-cloud-network.html">Designing an IPv6 Cloud network</a></li>
      <li><a href="https://docs.aws.amazon.com/whitepapers/latest/ipv6-on-aws/IPv6-on-AWS.html">IPv6</a></li>
    </ul>
  </li>
  <li>VPC documentation
    <ul>
      <li><a href="https://docs.aws.amazon.com/vpc/latest/userguide/aws-ipv6-support.html">Services that support IPv6</a>
(includes a list of ~30 services that have at least some IPv6 support; not listed at all are the ~200 other services)</li>
      <li><a href="https://docs.aws.amazon.com/vpc/latest/userguide/vpc-migrate-ipv6.html">Migrate your VPC from IPv4 to IPv6</a></li>
      <li><a href="https://docs.aws.amazon.com/vpc/latest/userguide/nat-gateway-nat64-dns64.html">DNS64 and NAT64</a></li>
    </ul>
  </li>
</ul>

<hr />

<p>Other links to this article: <a href="https://twitter.com/apparentorder/status/1704628704876388472">Twitter</a>,
<a href="https://news.ycombinator.com/item?id=37608900">Hacker News</a></p>

<p><em>Update 2023-09-21</em> – added “Links” section</p>

<p><em>Update 2023-09-21</em> – affected services: added RDS; made it clear that those are examples
(list is not exhaustive)</p>

<p><em>Update 2023-10-12</em> – added picture</p>

<p><em>Update 2023-10-24</em> – added links to Part Two</p>

<p><em>Update 2023-10-31</em> – minor edit for readability</p>

<hr />

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:byoip" role="doc-endnote">
      <p>IPv4 address charges only apply to AWS-owned IP addresses. It’s possible to “bring your own IP” (BYOIP), so if a
      customer happens to “own” a routable block of public IPv4 addresses (many large organizations do), they can use those
      addresses. Public “BYOIP” addresses will not be charged. <a href="#fnref:byoip" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:awsvpc" role="doc-endnote">
      <p>Interestingly, while EKS supports Fargate, it’s not possible to configure public IP addresses. And Fargate’s <code class="language-plaintext highlighter-rouge">awsvpc</code>
       networking mode is available on EC2-backed ECS as well, but doesn’t support public IP addresses there either.
       ECS/Fargate seems to be the only combination to allow this. <a href="#fnref:awsvpc" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:title" role="doc-endnote">
      <p>Thanks to u/slfyst for <a href="https://www.reddit.com/r/aws/comments/166itiw/cannot_escape_ipv4/">this reddit post</a>
      that I shamelessly stole the title from. <a href="#fnref:title" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:privatelink" role="doc-endnote">
      <p>It is possible to connect to many (but not all) services via
            <a href="https://aws.amazon.com/privatelink/">PrivateLink</a> instead, which is a direct connection into a customer VPC
            and requires no public IP addresses. But it is charged per service and per availability zone, about $9 per month
            per endpoint – so in order to connect to, say, Secrets Manager, EC2, SSM and SSM-Messages (yes, that’s separate) in
            three availability zones, that’s already over $1,200 per year and VPC. Additional traffic charges apply. <a href="#fnref:privatelink" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:dockerhub" role="doc-endnote">
      <p>So it’s not possible to launch ECS tasks from Elastic Container Registry, but it is possible from
          Dockerhub, which <em>does</em> support IPv6. Dockerhub also does rate limiting on pulls. <a href="#fnref:dockerhub" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[AWS will charge for IPv4 addresses but its IPv6 support lacks severely]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://tty.neveragain.de/assets/walking-alone.jpeg" /><media:content medium="image" url="https://tty.neveragain.de/assets/walking-alone.jpeg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">AWS: VPC DNS Support</title><link href="https://tty.neveragain.de/2021/09/07/aws-vpc-dns.html" rel="alternate" type="text/html" title="AWS: VPC DNS Support" /><published>2021-09-07T00:00:00+02:00</published><updated>2021-09-07T00:00:00+02:00</updated><id>https://tty.neveragain.de/2021/09/07/aws-vpc-dns</id><content type="html" xml:base="https://tty.neveragain.de/2021/09/07/aws-vpc-dns.html"><![CDATA[<h2 id="there-is-no-way-its-dns">There is No Way It’s DNS</h2>

<p>If you’ve been around in networking for a while, you probably know <a href="https://www.reddit.com/r/homelab/comments/5uwmtg/so_mad_with_this_ubiquiti_lr_ap_i_could_spit/">this haiku</a>:</p>

<p><img src="https://tty.neveragain.de/assets/dns-haiku.jpeg" alt="It's not DNS. There's no way it's DNS. It was DNS." /></p>

<p>Recently I’ve had one more experience like that, where enabling “VPC Hostnames” on AWS caused an outage. I’d have
sworn on my love of beer that it <em>cannot</em> be related to DNS.</p>

<p>Time for a closer look at DNS support in VPCs.</p>

<h2 id="overview-the-vpc-dns-resolver">Overview: The VPC DNS Resolver</h2>

<p>Amazon provides standard DNS resolvers in each VPC, free of charge. You can find the resolver service at several IP addresses:</p>

<ul>
  <li>IPv6: At the local address <code class="language-plaintext highlighter-rouge">fd00:ec2::253</code></li>
  <li>Legacy IP:
    <ul>
      <li>Local address <code class="language-plaintext highlighter-rouge">169.254.169.253</code></li>
      <li>VPC’s base network address plus 2 (e.g. 172.31.0.2 or 10.0.0.2) – therefore sometimes called the “dot-2 resolver”</li>
    </ul>
  </li>
</ul>

<p>This resolver is announced as part of the default <a href="https://docs.aws.amazon.com/vpc/latest/userguide/VPC_DHCP_Options.html#AmazonDNS">DHCP options</a>.</p>

<p>It can resolve both public internet DNS names (like <code class="language-plaintext highlighter-rouge">neveragain.de</code>) and VPC-specific hostnames.</p>

<p>The documentation of DNS support for VPCs <a href="https://docs.aws.amazon.com/vpc/latest/userguide/vpc-dns.html">fits on a single page</a>.</p>

<h2 id="overview-vpc-specific-dns-hostnames">Overview: VPC-Specific DNS Hostnames</h2>

<p>Each <a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-eni.html">Elastic network interface</a> (ENI) in a VPC can
have DNS hostnames pointing to it.</p>

<p>For EC2 instances, this would be <code class="language-plaintext highlighter-rouge">ec2-$IPADDRESS.$REGION.compute.amazonaws.com</code> (legacy IP only).</p>

<p>It works the same way for other in-VPC AWS services: They create ENIs in the VPC and some corresponding DNS records.
With RDS, for example, this is something like <code class="language-plaintext highlighter-rouge">mysql1234.cluster-oisdhosdahsj.eu-central-1.rds.amazonaws.com</code>.</p>

<p>RDS demonstrates an important distinction:</p>
<ul>
  <li>For private-only databases, they always (globally!) resolve to the private IP address</li>
  <li>For databases with public IPs:
    <ul>
      <li>queries from “outside” resolve to the public IP address</li>
      <li>queries from “inside” resolve to the private IP address</li>
    </ul>
  </li>
</ul>

<p>Here, “inside” usually means “from within the same VPC”, but this line is blurred by
<a href="https://docs.aws.amazon.com/vpc/latest/userguide/vpc-peering.html">VPC Peerings</a> and other connections like
<a href="https://docs.aws.amazon.com/vpc/latest/tgw/what-is-transit-gateway.html">Transit Gateway</a>.</p>

<h2 id="vpc-specific-dns-settings">VPC-Specific DNS settings</h2>

<p>There’s two settings to control the DNS resolver’s behavior. In the documentation’s own words:</p>

<ul>
  <li>
    <p><code class="language-plaintext highlighter-rouge">enableDnsHostnames</code>: Indicates whether instances with public IP addresses get corresponding public DNS hostnames<sup id="fnref:get" role="doc-noteref"><a href="#fn:get" class="footnote" rel="footnote">1</a></sup></p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">enableDnsSupport</code>: Indicates whether the DNS resolution is supported</p>
  </li>
</ul>

<p>Confusingly, the default settings vary a bit: “By default, both attributes are set to true in a default VPC or a VPC
created by the VPC wizard” – but any other VPC will have <code class="language-plaintext highlighter-rouge">enableDnsHostnames</code> <em>disabled</em> by default.</p>

<p><del>Reading</del> Naïvely skimming the VPC DNS documentation, the following assumption seemed sound:
<code class="language-plaintext highlighter-rouge">enableDnsHostnames</code> just creates additional DNS entries for IP addresses (authoritative DNS), and <code class="language-plaintext highlighter-rouge">enableDnsSupport</code>
controls whether the VPC’s resolver is enabled at all.</p>

<p>But, as we all know: Assumption is the mother of all fuck-ups.</p>

<h3 id="enablednssupport">enableDnsSupport</h3>

<p>This works pretty much as expected: If this is enabled, the VPC resolver IP addresses (see above) respond to queries.</p>

<p>Disabling it immediately casues those resolvers to go dark, i.e. any DNS query will not be answered (you’ll see a timeout).</p>

<p>It is also <em>required</em> for the next setting to have any effect.</p>

<h3 id="enablednshostnames">enableDnsHostnames</h3>

<p>As expected, this creates additional DNS records. That’s why this option is also <em>required</em> if you want to have public IPs
for RDS databases, for example.</p>

<p>Entirely <em>not</em> expected – for me, at least – is that it sythesizes forward and reverse responses for any private IP
address. That’s right: Not just the private IP addresses within your VPC CIDR range, but <em>any</em> private IP address. Like, you
know, those private IP addresses that you’re using on-premises.</p>

<p>This seems reasonable for VPC addresses:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[ec2-user@ip-10-0-0-34 ~]$ dig +noall +ans -x 10.0.0.34
34.0.0.10.in-addr.arpa.	285	IN	PTR	ip-10-0-0-34.eu-central-1.compute.internal.
</code></pre></div></div>

<p>But now your on-premises servers have a reverse lookup, too! From the VPC’s perspective, at least:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[ec2-user@ip-10-0-0-34 ~]$ dig +noall +ans -x 192.168.123.234
234.123.168.192.in-addr.arpa. 600 IN	PTR	ip-192-168-123-234.eu-central-1.compute.internal.

[ec2-user@ip-10-0-0-34 ~]$ dig +noall +ans -x 172.16.0.3
3.0.16.172.in-addr.arpa. 600	IN	PTR	ip-172-16-0-3.eu-central-1.compute.internal.
</code></pre></div></div>

<p>If this option is disabled, those queries return <code class="language-plaintext highlighter-rouge">NXDOMAIN</code> (entry does not exist), as they should.</p>

<p>This is documented, but rather in the fine print instead of obviously with the options’ descriptions.</p>

<h2 id="resolution-pun-inteded">Resolution (Pun Inteded)</h2>

<p>So, after all: It was DNS. The client in question performs a reverse lookup of the server’s IP address and then
uses that information to see which credentials it should present. After enabling VPC DNS hostnames, that lookup
was no longer answered with “no such entry” but with the synthesized <code class="language-plaintext highlighter-rouge">ip-192-168-0-123.eu-central-1.compute.internal</code>.
Subsequently, the credentials lookup failed to find a match.</p>

<h2 id="additional-trivia">Additional Trivia</h2>

<h3 id="version-query">Version Query</h3>

<p>This is something I’ve observed often on AWS: They seem to like easter eggs and/or proper implementations.</p>

<p>From ancient times, when <a href="https://www.isc.org/bind/">ISC BIND</a> was the de-facto authoritative DNS server software, you’d
query a nameserver’s version information from the <code class="language-plaintext highlighter-rouge">version.bind</code> hostname in the <em>Chaos</em> (<code class="language-plaintext highlighter-rouge">CH</code>) class (as opposed to the
default <em>Internet</em> class <code class="language-plaintext highlighter-rouge">IN</code>). The nameserver would answer with the software version, e.g. <code class="language-plaintext highlighter-rouge">"9.11.0"</code>.</p>

<p>Most modern nameservers simply don’t answer this query. The VPC resolver, however, does:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[ec2-user@ip-10-0-0-34 ~]$ dig +noall +ans version.bind txt ch @10.0.0.2
version.bind.           0       CH      TXT     "EC2 DNS"
</code></pre></div></div>

<h3 id="caching">Caching</h3>

<p>The resolver IP address seems to employ some caching of its own, apparently for a few minutes per record.</p>

<p>This is important when changing the VPC Hostnames setting: It will not take effect immediately, and not consistently!</p>

<h3 id="fleet">Fleet</h3>

<p>I can observe fluctuating time-to-live values returned by the resolver IP address, which leads me to believe that there’s
actually a handful of different resolvers answering the queries:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[ec2-user@ip-10-0-0-34 ~]$ while true; do dig +noall +ans neveragain.de soa @10.0.0.2; sleep 1; done
neveragain.de.          243     IN      SOA     squigley.hq.neveragain.de. hostmaster.neveragain.de. 2021080901 86400 3600 2419200 10800
neveragain.de.          242     IN      SOA     squigley.hq.neveragain.de. hostmaster.neveragain.de. 2021080901 86400 3600 2419200 10800
neveragain.de.          241     IN      SOA     squigley.hq.neveragain.de. hostmaster.neveragain.de. 2021080901 86400 3600 2419200 10800
neveragain.de.          247     IN      SOA     squigley.hq.neveragain.de. hostmaster.neveragain.de. 2021080901 86400 3600 2419200 10800
neveragain.de.          245     IN      SOA     squigley.hq.neveragain.de. hostmaster.neveragain.de. 2021080901 86400 3600 2419200 10800
neveragain.de.          246     IN      SOA     squigley.hq.neveragain.de. hostmaster.neveragain.de. 2021080901 86400 3600 2419200 10800
[ ... and so on ...]
</code></pre></div></div>

<h2 id="additional-reading">Additional Reading</h2>

<p>For advanced DNS resolution topics, see also:</p>
<ul>
  <li><a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resolver.html">Route53 Resolver Endpoints</a> to forward DNS queries
between external (e.g. on-premises) DNS and AWS (beware: while this sounds like three lines of BIND configuration, it actually
starts at almost $190/month!)</li>
  <li><a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resolver-dnssec-validation.html">DNSSEC validation</a> (not enabled by default, unfortunately)</li>
  <li><a href="https://aws.amazon.com/about-aws/whats-new/2021/04/amazon-route-53-resolver-dns-firewall-generally-available/?nc1=h_ls">DNS Firewall</a>,
which is a corny name for hostname-specific override rules in the resolver (you can fake <code class="language-plaintext highlighter-rouge">NODATA</code>, <code class="language-plaintext highlighter-rouge">NXDOMAIN</code> and arbitrary
<code class="language-plaintext highlighter-rouge">CNAME</code> responses)</li>
  <li>For VPC Peerings, there’s <a href="https://docs.aws.amazon.com/vpc/latest/peering/modify-peering-connections.html">separate settings for DNS</a></li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>It was DNS.</p>

<hr />

<p><a href="https://twitter.com/apparentorder/status/1435241174604947462">Discuss and/or follow on Twitter!</a></p>

<hr />

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:get" role="doc-endnote">
      <p>Minor gripe: What does that even mean, an instance <em>gets</em> a hostname? What exactly does it get, and how?
    Does it affect the DHCP-assigned hostname as well? A rather weak choice of words, I’d say, used throughout
    the document. Spoiler: It does not affect DHCP responses. <a href="#fnref:get" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[A closer look at DNS support in VPCs -- and its surprising side-effects]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://tty.neveragain.de/assets/dns-haiku-small.jpeg" /><media:content medium="image" url="https://tty.neveragain.de/assets/dns-haiku-small.jpeg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Timeless Services</title><link href="https://tty.neveragain.de/2021/06/29/timeless-services.html" rel="alternate" type="text/html" title="Timeless Services" /><published>2021-06-29T00:00:00+02:00</published><updated>2021-06-29T00:00:00+02:00</updated><id>https://tty.neveragain.de/2021/06/29/timeless-services</id><content type="html" xml:base="https://tty.neveragain.de/2021/06/29/timeless-services.html"><![CDATA[<hr />

<p><em>This post is part of a blog series: <a href="/2021/04/26/road-to-reweb.html">Road to re:Web</a>.</em></p>

<hr />

<h2 id="an-anti-cloud-pattern">An Anti-Cloud Pattern</h2>

<p>Idle resources are an anti-cloud pattern. As I see it, any service that charges by provisioned time (per second / hour / month)
– regardless of actual workload – isn’t truly serverless. Resources are provisioned exclusively for a single tenant.
For all practical purposes, they are always used at full capacity. But most of the time, they are idle.</p>

<p>It’s like leaving my car running when I get home.</p>

<p>Not being time-based – not blocking resources that are not actively used – should be a property of what’s called
<em>serverless</em>. But much of the world already has another definition (e.g. <a href="https://aws.amazon.com/fargate/">Fargate</a> is
considered serverless, just because there are no servers to manage).</p>

<p>Therefore I’ll be calling those services <em>timeless</em>. Until I come up with a better name.</p>

<p>This post will focus on AWS, but the principles are the same for all cloud providers.</p>

<h2 id="time-based-services-hurt-everyone">Time-Based Services Hurt Everyone</h2>

<p>As I <a href="/2021/04/27/aws-billing-leap-of-faith.html">laid out before</a>, cloud cost is problematic for many users. With the typical
services that are time-based, like EC2 or Fargate, costs are significantly higher for almost all usage patterns.
This is especially true for any non-production environment, as well as individuals (students, developers, freelancers
etc.) and many small or start-up organizations.</p>

<p>Also I cannot “forget” to shut down a <em>timeless</em> service. Many people have received panic-inducing bills
– simply because some resources were left running, but idle
(here is one <a href="https://cloudirregular.substack.com/p/please-fix-the-aws-free-tier-before">recent example</a> of many).
While this isn’t the only cloud billing trap, it’s a big one. And certainly not just for novices – especially larger 
companies tend to have quite a few resources running 24x7 that they don’t need.</p>

<p>Most importantly: Idle resources are waste. It’s simply not necessary. Besides wasting money, it has significant environmental
impact. This impact is improved by being “in the cloud” instead of your grandma’s on-premises data center, because large cloud
providers are really good at reducing excess waste (e.g. from cooling and power units) – but overall, it’s not much better.</p>

<h2 id="aint-easy-bein-green">Ain’t Easy Bein’ Green</h2>

<p>The sad thing is: It’s really not easy to architect exclusively using timeless services.</p>

<p>Often it’s due to the application’s requirements, for example, when it needs a classic relational database like MySQL or
PostgreSQL. <a href="https://aws.amazon.com/rds/aurora/serverless/">Aurora Serverless v1</a> helps a bit, but isn’t quite there yet (v2 looks promising!) – and doesn’t fit for all use-cases.</p>

<p>And sometimes it feels like AWS is making this difficult on purpose – most notably with VPC design. Let’s take a closer
look at that.</p>

<h3 id="nat-gateway">NAT Gateway</h3>

<p>On AWS, the arch enemy, of course, is the
<a href="https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html">Managed NAT Gateway</a>.</p>

<p>This is required<sup id="fnref:nat" role="doc-noteref"><a href="#fn:nat" class="footnote" rel="footnote">1</a></sup> if I want to have a non-public network (a very basic security requirement in many scenarios) that
can do outbound connections to the internet – like, you know, for downloading security updates, or using AWS APIs.</p>

<p>This hits home for almost $40 per month without having moved a single network packet. Those cost extra, of course.
Not to mention that you need one of these fuckers for <em>each</em> availability zone, if they need to be redundant. A laughable cost
for most businesses – but absolutely out of the question for any individual.</p>

<h3 id="domino-effect">Domino Effect</h3>

<p>So a key desire is to not put myself in a situation where I need a NAT Gateway.</p>

<p>Take the Lambda / RDS pair as an example.</p>

<p>If both can exist on a private network and do not need outbound connectivity, everything is fine.</p>

<p>But as soon as my Lambda needs something from the internet or from an AWS API, it gets complicated:</p>

<ul>
  <li>
    <p>I could remove the Lambda from my VPC. Then it will have a public IP address, and it will happily connect to the world.
Unfortunately, it will no longer be able to talk to my private network, where RDS is living.</p>
  </li>
  <li>
    <p>I could remove the Lambda from my VPC <em>and</em> place RDS in a public subnet, so it will have a public IP. The Lambda could
connect to that. But a database with a public IP address is considered a rather strong red flag in IPv4-land, and I
couldn’t even use a Security Group to limit access properly. Not to mention that while vanilla RDS can have a public
IP address, Aurora databases can <em>not</em> – annihilating any dreams about the timeless
<a href="https://aws.amazon.com/rds/aurora/serverless/">Aurora Serverless v2</a> right away.</p>
  </li>
  <li>
    <p>I could move Lambda to a public subnet within my VPC. This would solve all my problems. It could get a public IP <em>and</em> could
connect to any private subnet I have. Unfortunately, this isn’t supported:
<a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html">Lambda will <em>not</em> pick up a public IP address in a public subnet</a> – probably because that would be too easy.</p>
  </li>
  <li>
    <p>If I just need access to an AWS API, I could use <a href="https://docs.aws.amazon.com/vpc/latest/privatelink/integrated-services-vpce-list.html">PrivateLink</a> to map endpoints into my private subnet – but those are time-based as well: $9 per month, per availability
zone, per AWS service. Right.</p>
  </li>
</ul>

<p>There are similarly weird examples. To name one, the network mode <em>awsvpc</em> in ECS will happily grab a public IP address
when used with ECS-Fargate. If I use the same <em>awsvpc</em> network mode with ECS-EC2 on a VPC’s public subnet, it will
<a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/service-configure-network.html">refuse to pick up a public IP address</a>.</p>

<h3 id="yesterdays-internet">Yesterday’s Internet</h3>

<p>Just to add insult to injury: All this drama completely goes away with IPv6. There is no NAT in IPv6, and the VPC would use
a regular <a href="https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Internet_Gateway.html">Internet Gateway</a>, or to mimic the
NAT Gateway’s outbound-only characteristic, an <a href="https://docs.aws.amazon.com/vpc/latest/userguide/egress-only-internet-gateway.html">Egress-only Internet Gateway</a>.</p>

<p>Both are free!</p>

<p>Unfortunately, in 2021, it still is virtually impossible to use a IPv6-only VPC<sup id="fnref:ipv6" role="doc-noteref"><a href="#fn:ipv6" class="footnote" rel="footnote">2</a></sup>.</p>

<h2 id="defining-cloud">Defining Cloud</h2>

<p>Everyone has their own definiton of cloud. To me, it’s using abstractions as much as possible. This, in turn, means that
I can focus on what is interesting to me. All the infrastructure, all the software stack and runtimes below, making that
secure and highly available – those are solved problems. I don’t want to do that myself.</p>

<p>Following that route of abstraction, we end up at truly serverless – <em>timeless</em> – services.</p>

<p>To me, those are inconsequential configuration items. Take S3 buckets, for example. I can have one or ten of those. It’s
just a thing that’s there. I pay for actual data storage – not for merely having an empty bucket around.</p>

<p>Here’s some examples of time-based services:</p>
<ul>
  <li>NAT Gateway, oh yes</li>
  <li>RDS and Aurora (at least in their non-Serverless flavors)</li>
  <li>EC2</li>
  <li>Fargate</li>
  <li>Elastic Loadbalancer</li>
  <li>PrivateLink</li>
  <li>EKS</li>
  <li>VPN</li>
</ul>

<p>Some wonderful examples of timeless services:</p>
<ul>
  <li>VPC itself (often overlooked!)</li>
  <li>S3</li>
  <li>Lambda</li>
  <li>EFS</li>
  <li>Timestream</li>
  <li>Infinidash</li>
  <li>API Gateway</li>
  <li>DynamoDB</li>
  <li>SQS</li>
</ul>

<p>Noteworthy is the new <a href="/2021/06/18/app-runner-deep-dive.html">App Runner service</a>: it lands somewhere inbetween, because
it uses a hybrid approach (CPU is timeless, RAM is not) – a very interesting step in the right direction.</p>

<h3 id="prototyping-without-regrets">Prototyping Without Regrets</h3>

<p>Note that I can configure all of these timeless services and build an architecture that’s highly available across multiple
availability zones, needs zero maintenance, scales automatically and costs <em>literally nothing</em> until I really use it.
And even then it’s <em>so</em> much cheaper.</p>

<p>As an example: A while back, I built a backend for an app. The app never went anywhere, but this highly scalable
timeless infrastructure was available for more than a year, and still is – and if the app would now be an overnight
success, this architecture would scale with it, without me doing anything. I payed a few cents for all that, in total.</p>

<p>Try that kind of worry-free prototyping with time-based services…</p>

<h2 id="conclusion">Conclusion</h2>

<p>Time-based services are an anti-cloud pattern. Yes, you <em>could</em> turn them on and off as needed, or have some pilot light
with auto-scaling that would eventually react after a few minutes.</p>

<p>But that’s not what cloud is about. As cloud services evolve and time goes on, we’ll see more architectures that are fully
built on timeless services, allowing everyone to worry less about costs and to focus on their actual work.</p>

<p>In a sense, <a href="https://reweb.neveragain.de/">re:Web</a> is about building a bridge to timeless services: It makes it possible
to run common web applications on Lambda instead of EC2 or Fargate (or App Runner).</p>

<hr />

<p><a href="https://twitter.com/apparentorder/status/1410659850959343617">Discuss and/or follow on Twitter!</a></p>

<hr />

<p><em>This post is part of a blog series: <a href="/2021/04/26/road-to-reweb.html">Road to re:Web</a>.</em></p>

<hr />

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:nat" role="doc-endnote">
      <p>Technically, you could run your own EC2 box (then called a NAT <em>instance</em>) for like $3/month. Per availability zone. And then take care of its configuration, redundancy, security updates, monitoring and all the other management issues yourself. But the point of cloud is to do <em>less</em> of what AWS calls <a href="https://aws.amazon.com/blogs/aws/we_build_muck_s/">“undifferentiated heavy lifting”</a>. <a href="#fnref:nat" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:ipv6" role="doc-endnote">
      <p>Note that top-level <a href="https://en.wikipedia.org/wiki/IPv4_address_exhaustion">IPv4 exhaustion</a> occurred more than ten years ago. <a href="#fnref:ipv6" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[Time-based services are an anti-cloud pattern]]></summary></entry><entry><title type="html">AWS: App Runner Deep Dive</title><link href="https://tty.neveragain.de/2021/06/18/app-runner-deep-dive.html" rel="alternate" type="text/html" title="AWS: App Runner Deep Dive" /><published>2021-06-18T00:00:00+02:00</published><updated>2021-06-18T00:00:00+02:00</updated><id>https://tty.neveragain.de/2021/06/18/app-runner-deep-dive</id><content type="html" xml:base="https://tty.neveragain.de/2021/06/18/app-runner-deep-dive.html"><![CDATA[<p>Launched last month, <a href="https://aws.amazon.com/apprunner/">AWS App Runner</a> gets a lot of things right to simplify
spinning up simple web applications – to the point that no prior knowledge of all the AWS infrastructure bells
and whistles is required.</p>

<p>There’s also a new mode of billing, which is very exciting to me.</p>

<p>Let’s take a look, especially at the technical details!</p>

<h2 id="overview">Overview</h2>

<p>The concept is awesome: App Runner is what happens when you mix CodeBuild, CodeDeploy, Beanstalk, Lambda and
Fargate in a vial, shake it thoroughly, and then distill it into something beautifully simple.</p>

<p>It aims to provide the whole shebang from source code to production, while taking care of seamless deployments. I just
plug in my web application, hit play, and I have a TLS-secured load-balanced application running in multiple
availability zones. And it autoscales out of the box! That’s pretty cool.</p>

<p>To say this again: Virtually no prior AWS-specific knowledge is required. I don’t need to know anything about VPC networking.
I don’t have to fumble with Elastic Loadbalancing for half an hour. I don’t need to know who ACM even is, or Fargate,
or ECS, or – heaven forbid – EKS.</p>

<p>Note that I said <em>web</em> application: App Runner is explicitly designed for something that speaks HTTP, is health-checkable
by HTTP and is used from the outside world via HTTP. And, needless to say, the application needs to be stateless – just like
any web application that wants to be loadbalanced and scaled properly.</p>

<p>App Runner operates on containers. I can either simply use some container from a registry.</p>

<p>Or I can have App Runner build a container for me. But we’re not talking about the classic mode of “here’s my Dockerfile, go build
that”. Instead, App Runner provides base images for some programming languages. It connects to a Github repository, places
the actual source code in the base image, and builds a container from that. This is actually nice, because I don’t have to
worry about maintaining and security-patching the base image. And this “just add source code” concept is already known from AWS
Lambda. It’s magic.</p>

<h2 id="exciting-new-billing-model">Exciting New Billing Model</h2>

<p>I’m really excited about a feature that got little attention: CPU pricing is only charged when the application is
actually handling requests. Repeat: <strong>An idle application will not incur any CPU charges</strong>.</p>

<p>This brings the whole model much closer to what AWS Lambda does. For almost all applications, that’s a <em>significant</em>
reduction. Not only for development and QA environments, but also for most production applications. While Lambda can
do this per request with millisecond precision, App Runner does this by the second, which already is great.</p>

<p>Keep in mind that on AWS, paying less translates pretty well into less waste of resources – and therefore
to my application’s carbon footprint. Less waste is good for everyone, not just for the CFO.</p>

<p>App Runner can do this easily because it’s in control of the loadbalancing. It knows when there are no active requests.</p>

<p>In contrast to Lambda, App Runner cannot fully stop an application when there are no requests. There may be background
threads, cron jobs and so on. Also, the container’s startup time might be way too high to have the next request wait for it.</p>

<p>The secret here is that App Runner keeps the application running, but it throttles the CPU massively. This is a brilliant
solution for containerized applications. And it’s a huge step towards the vision that I’m driving with
<a href="https://reweb.neveragain.de/info/">re:Web</a>.</p>

<p>This is also why only CPU charges are dynamic; memory is charged for the whole time the application is available – but
memory pricing isn’t too significant in comparison.</p>

<p>An awesome next step would be if App Runner allowed to opt-in to fully stopping the application, so there is actually
no instance running while there are no requests – like Lambda does.</p>

<h2 id="technical-deep-dive">Technical Deep Dive</h2>

<p>Enough chitchat. You can get all the usual details from the <a href="https://aws.amazon.com/blogs/containers/introducing-aws-app-runner/">AWS blog</a>
or the <a href="https://aws.amazon.com/blogs/aws/app-runner-from-code-to-scalable-secure-web-apps/">AWS blog</a>.</p>

<p>Let’s take a look under the hood, as far as we can do that from a customer’s perspective.</p>

<h3 id="building">Building</h3>

<p>Building from Github is actually the only process where I get proper log output. It looks very much like I’d expect
a Docker build to look:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[AppRunner] Starting to build your application source code.
[Build] foo
[Build] Sending build context to Docker daemon  9.216kB
[Build] Step 1/5 : FROM 172267607524.dkr.ecr.eu-west-1.amazonaws.com/awsfusionruntime-python3:3.8.5
[Build] 3.8.5: Pulling from awsfusionruntime-python3
[...]
[Build] Status: Downloaded newer image for 172267607524.dkr.ecr.eu-west-1.amazonaws.com/awsfusionruntime-python3:3.8.5
[Build]  ---&gt; cca0c7fe0d48
[Build] Step 2/5 : COPY . /app
[Build]  ---&gt; 1809bc3e008b
[Build] Step 3/5 : WORKDIR /app
[Build]  ---&gt; Running in 1c8671d6366f
[Build] Removing intermediate container 1c8671d6366f
[Build]  ---&gt; 5a491f6a232a
[Build] Step 4/5 : RUN echo foo
[Build]  ---&gt; Running in 33150a894724
[Build] foo
[Build] Removing intermediate container 33150a894724
[Build]  ---&gt; 23df2b9f7940
[Build] Step 5/5 : EXPOSE 8080
[Build]  ---&gt; Running in 0d6f001a39d7
[Build] Removing intermediate container 0d6f001a39d7
[Build]  ---&gt; e0d9f18a3c50
[Build] Successfully built e0d9f18a3c50
[Build] Successfully tagged application-image:latest
</code></pre></div></div>

<p>In case you’re wondering: <code class="language-plaintext highlighter-rouge">echo foo</code> is my build command, because I was forced to give one.</p>

<p>When not using the Console, the build configuration is maintained in <code class="language-plaintext highlighter-rouge">apprunner.yml</code>, as part of the repository. A simple
example looks like this:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>version: 1.0
runtime: python3 
build:
  commands:
    build:
      - pip install pipenv
      - pipenv install 
run: 
  command: python app.py
</code></pre></div></div>
<p>(taken from <a href="https://docs.aws.amazon.com/apprunner/latest/dg/config-file-examples.html">the documentation</a>)</p>

<p>One small weirdness: The resulting container will have the generated <code class="language-plaintext highlighter-rouge">Dockerfile</code> in <code class="language-plaintext highlighter-rouge">/app</code>. Nothing to see there, though.</p>

<p>Sadly, I was not allowed to pull the <code class="language-plaintext highlighter-rouge">awsfusionruntime-python3</code> image myself.</p>

<h3 id="architecture">Architecture</h3>

<p>From what I can tell, App Runner isn’t built on Application Loadbalancer. There seems to be a huge fleet of <a href="https://www.envoyproxy.io">envoy</a>
proxies handling the requests from the internet.</p>

<p>The application container itself runs on an AWS-owned ECS cluster.</p>

<p>App Runner isn’t orchestrating other AWS services for me – it encapsulates them. This means that any resources
used by App Runner will <em>not</em> show up in my account. I won’t see the ACM certificate or the ECS service, for example.
Luckily, this also means that I will not have the usual IAM headaches to grant carefully scoped access to other AWS
services.</p>

<h3 id="dns">DNS</h3>

<p>The DNS record returns three IPs (even when only one instance is available), which I assume is one per availability zone.
But for reasons unknown, App Runner takes a quick CNAME detour to explore the length limits of DNS names:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ysvp2d42qc.eu-west-1.awsapprunner.com. 60 IN CNAME 8a3b877e0fff4ad5abcbb491626a7531.5ysp12zfctly24v37dziw9tv9vgyog35m8cxbz6vyafmg15s70.6dd878t7dna7cgqgxld7zry5aq57tlrwg7uq14fk6g3sr656w4.eu-west-1.awsapprunner.com.
8a3b877e0fff4ad5abcbb491626a7531.5ysp12zfctly24v37dziw9tv9vgyog35m8cxbz6vyafmg15s70.6dd878t7dna7cgqgxld7zry5aq57tlrwg7uq14fk6g3sr656w4.eu-west-1.awsapprunner.com. 60 IN A 54.75.254.235
8a3b877e0fff4ad5abcbb491626a7531.5ysp12zfctly24v37dziw9tv9vgyog35m8cxbz6vyafmg15s70.6dd878t7dna7cgqgxld7zry5aq57tlrwg7uq14fk6g3sr656w4.eu-west-1.awsapprunner.com. 60 IN A 34.255.134.39
8a3b877e0fff4ad5abcbb491626a7531.5ysp12zfctly24v37dziw9tv9vgyog35m8cxbz6vyafmg15s70.6dd878t7dna7cgqgxld7zry5aq57tlrwg7uq14fk6g3sr656w4.eu-west-1.awsapprunner.com. 60 IN A 52.210.161.22
</code></pre></div></div>
<p>I don’t know how I feel about the 60 seconds TTL – seems like introducing a lot of delay for repeated lookups.</p>

<p>App Runner’s DNS names do not use DNSSEC.</p>

<h3 id="tls">TLS</h3>

<p>There is no support for TLS 1.3 (bad). Only version 1.2 is supported, i.e. no older TLS versions (good).</p>

<p>Certificates for custom domain names are created with Certificate Transparency.</p>

<p>The <a href="https://www.ssllabs.com/ssltest/index.html">Qualys SSL Labs server test</a> grades an App Runner-hosted site as <em>A</em> (I’d assume that <em>A+</em> were possible
if the application emits <a href="https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security">HSTS</a> headers).</p>

<h3 id="frontend">Frontend</h3>

<p>Most notably, App Runner-hosted applications will let everyone know they’re running behind envoy in the HTTP headers:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>server: envoy
x-envoy-upstream-service-time: 2005
</code></pre></div></div>

<p>This even happens for successful requests. It’s rather irritating that a proxy <em>overwrites</em> my <code class="language-plaintext highlighter-rouge">Server</code> header.</p>

<p>Also there is no support for HTTP/1.0 (envoy limitation). While this isn’t an issue for modern browsers, some tools still
default to HTTP/1.0 (Python’s <a href="https://docs.python.org/3/library/http.server.html#http.server.BaseHTTPRequestHandler.protocol_version">http.server</a>
and <a href="https://httpd.apache.org/docs/2.4/programs/ab.html">ApacheBench</a>, for example).</p>

<p>During my tests, I could see about 30 different proxy IPs, all from the 10.0.0.0/16 range. The TCP connection comes from
some link-local address.</p>

<p>In the access log, it looks like this:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>169.254.175.249 - - [18/Jun/2021 14:07:50] "GET / HTTP/1.1" 200 -
</code></pre></div></div>

<p>And <code class="language-plaintext highlighter-rouge">X-Forwarded-For</code> looks like this (where the first one is my obfuscated client IP):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>X-Forwarded-For: 37.123.234.345, 10.0.187.62
</code></pre></div></div>

<p>Loadbalancing seems to be simple round-robin or connection count or something – I did not observe any kind of persistence,
neither with a cookie nor by client IP. I consider this a good thing.</p>

<p>I tried three instances concurrently, and they were placed in eu-west-1{a,b,c}, so I’m guessing App Runner always spreads
them evenly.</p>

<p>By the way: When the service is paused, envoy returns HTTP 404 (Not found). I’m not sure if that’s a smart move, especially
with regard to search engine crawlers etc.</p>

<h3 id="container-execution--ecs">Container execution / ECS</h3>

<p>The container will find itself in an ECS environment. This comes with the expected environment variables being injected:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>AWS_EXECUTION_ENV=AWS_ECS_FARGATE
AWS_DEFAULT_REGION=eu-west-1
AWS_REGION=eu-west-1
ECS_CONTAINER_METADATA_URI=http://169.254.170.2/v3/58f40ddf9c4b42fc83bfc6c7fe1a1cd8-193386898
ECS_CONTAINER_METADATA_URI_V4=http://169.254.170.2/v4/58f40ddf9c4b42fc83bfc6c7fe1a1cd8-193386898
[...]
</code></pre></div></div>

<p>Tickling <code class="language-plaintext highlighter-rouge">$ECS_CONTAINER_METADATA_URI_V4/task</code> reveals some interesting bits:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
 "Cluster": "arn:aws:ecs:eu-west-1:172068907549:cluster/bullet-srv-999999999999",
 "TaskARN": "arn:aws:ecs:eu-west-1:172068907549:task/bullet-srv-999999999999/58f40ddf9c4b42fc83bfc6c7fe1a1cd8",
 "Family": "bullet-td-6b4120addb68419590ea47b94506ed43-8777b77689784d7a9cd6815f70cf4bc5",
 "Revision": "1",
 "Containers": [
   {
     "DockerId": "58f40ddf9c4b42fc83bfc6c7fe1a1cd8-193386898",
     "Name": "instance",
     "DockerName": "instance",
     "Image": "736217495344.dkr.ecr.eu-west-1.amazonaws.com/image-repo-6b4120addb68419590ea47b94506ed43@sha256:02b8e350b106f837b1346b52e64590281ed953fd506737779f6a59072d4c5cdb",
     "ImageID": "sha256:02b8e350b106f837b1346b52e64590281ed953fd506737779f6a59072d4c5cdb",
     "Labels": {
       "com.amazonaws.ecs.cluster": "arn:aws:ecs:eu-west-1:172068907549:cluster/bullet-srv-999999999999",
       "com.amazonaws.ecs.container-name": "instance",
       "com.amazonaws.ecs.task-arn": "arn:aws:ecs:eu-west-1:172068907549:task/bullet-srv-999999999999/58f40ddf9c4b42fc83bfc6c7fe1a1cd8",
       "com.amazonaws.ecs.task-definition-family": "bullet-td-6b4120addb68419590ea47b94506ed43-8777b77689784d7a9cd6815f70cf4bc5",
       "com.amazonaws.ecs.task-definition-version": "1"
     },
     "Limits": {
       "CPU": 960
     },
   },
   {
     "DockerId": "58f40ddf9c4b42fc83bfc6c7fe1a1cd8-329321356",
     "Name": "aws-fargate-request-proxy",
     "DockerName": "aws-fargate-request-proxy",
     "Image": "172068907549.dkr.ecr.eu-west-1.amazonaws.com/aws-fargate-request-proxy:cell9",
     "ImageID": "sha256:71bb81a8e1a15726cc5711f7334e11ccf27f53504724128a75bf3915ace1960b",
     "Labels": {
       "com.amazonaws.ecs.cluster": "arn:aws:ecs:eu-west-1:172068907549:cluster/bullet-srv-999999999999",
       "com.amazonaws.ecs.container-name": "aws-fargate-request-proxy",
       "com.amazonaws.ecs.task-arn": "arn:aws:ecs:eu-west-1:172068907549:task/bullet-srv-999999999999/58f40ddf9c4b42fc83bfc6c7fe1a1cd8",
       "com.amazonaws.ecs.task-definition-family": "bullet-td-6b4120addb68419590ea47b94506ed43-8777b77689784d7a9cd6815f70cf4bc5",
       "com.amazonaws.ecs.task-definition-version": "1"
     },
     "Limits": {
       "CPU": 64,
       "Memory": 40
     }
   }
 ],
 "Limits": {
   "CPU": 1,
   "Memory": 2048
 },
 "AvailabilityZone": "eu-west-1c"
}
</code></pre></div></div>
<p>(boring parts removed; and <code class="language-plaintext highlighter-rouge">999999999999</code> is where <em>my</em> account-id appeared)</p>

<p>I wonder what the story behind <em>bullet</em> is. It seems to be the internal name of App Runner, or they changed the name last-minute. Additional
data point: At some point, the Console used to <a href="https://twitter.com/coates/status/1399408582341312513">link to <code class="language-plaintext highlighter-rouge">bullet.yml</code></a> instead of <code class="language-plaintext highlighter-rouge">apprunner.yml</code>.</p>

<p>That <code class="language-plaintext highlighter-rouge">fargate-request-proxy</code> sounds interesting, so I tried pulling those images, of course. I wasn’t allowed to.</p>

<p>Other than that, I didn’t notice anything out of the ordinary.</p>

<h3 id="auto-scaling">Auto-Scaling</h3>

<p>Scaling happens quite quickly.</p>

<p>I’ve configured that each container instance can stomach ten concurrent connections.</p>

<p>Then I overwhelmed the service by going immediately from 0 connections (idle container instance) to 40 concurrent connections.
The available container instance responded immediately, as it should. A second
container instance started serving requests just 22 seconds later, and the third instance another 28 seconds later.</p>

<p>The maximum number of instances was configured at three, so a fourth instance was never started.</p>

<p>The important part is to configure the concurrency per container instance lower than what it actually can handle. That leaves
some headroom until scaling kicks in – or in case one container instance (or even a whole availability zone) fails.</p>

<p>I’d say scaling reacts fast enough for pretty much all use-cases (unless you’re expecting <a href="https://www.reddit.com/r/adventofcode/comments/k9lt09/postmortem_2_scaling_adventures/">Advent of Code-style traffic peaks</a>).</p>

<h3 id="throttling">Throttling</h3>

<p>Now this is super interesting.</p>

<p>As mentioned earlier, the application gets throttled almost immediately when there are no active requests.</p>

<p>Let’s put a number on that. Running some python code that does 10,000 iterations of random math stuff, here’s how long that takes:</p>
<ul>
  <li>~0.9 seconds on my 2017 iMac</li>
  <li>~2.5 seconds on App Runner with 1 CPU configured, while active</li>
  <li>~150 seconds while inactive</li>
</ul>

<p>Yes, 2.5 minutes. The task is throttled by a factor of 60. While that throttled CPU time is actually free, I’ll assume that
mining coins still isn’t worth it, even if memory pricing is small.</p>

<h2 id="limitations">Limitations</h2>

<ul>
  <li>Source code: Github only</li>
  <li>Images: ECR only, but ECR Public is supported</li>
  <li>Managed runtimes: Python and Node only</li>
  <li>No VPC support (therefore no EFS support either)</li>
  <li>No support for IPv6, of course</li>
  <li>No support for secrets (Secrets Manager / Parameter Store)</li>
  <li>No support for <a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-exec.html">ECS Exec</a></li>
  <li>No support for WAF</li>
  <li>No support for ARM (amd64 / “Intel” only)</li>
  <li>No support for HTTP/2</li>
  <li>HTTP/1.0 is not supported</li>
  <li>Backend: no HTTPS support between App Runner proxies and the application – which is perfectly fine with me, because nobody needs that
within a VPC, really (even if it’s a VPC in an AWS-owned account)</li>
  <li>Frontend: TLS version 1.2 <em>only</em></li>
  <li>Frontend: plain HTTP is <em>not</em> supported: port 80 is blocked (will time out) – I think a forced redirect to HTTPS would have made more sense</li>
  <li>Scaling based on concurrent connections only</li>
</ul>

<p>Many of those items are already on the <a href="https://github.com/aws/apprunner-roadmap">roadmap</a>.</p>

<h2 id="premature-launch">Premature Launch</h2>

<p>What follows is a “grab bag” of nuisances. App Runner is a mere month old now, but still, it feels surprisingly
unpolished. Maybe it would have been worth delaying the launch a bit.</p>

<p>That being said, this is all minor stuff and I’m sure they will be resolved quickly.</p>

<ul>
  <li>Initial setup of Github causes a pop-up window – it took me a minute or two until I realized this (Safari is very good at hiding blocked pop-ups)</li>
  <li>Maybe it’s because I’m a Github noob, but this sentence confused me just as much: “Choose from locations where a GitHub app is already installed or install another elsewhere in your account.” (I didn’t even know Github <em>had</em> “apps”)</li>
  <li>The lack of CodeCommit support at launch is… an interesting statement</li>
  <li>Completing the Github connection gave me this meaningful error: <code class="language-plaintext highlighter-rouge">null is not an object (evaluating 'Ke.postMessage')</code> –
but, as I figured out later, it was successful anyway</li>
  <li>There is a message “Deleting foo” <a href="https://twitter.com/apparentorder/status/1403051519780478978">when you click refresh</a> (that was a new one for me)</li>
  <li>The application setup page needs a clear hint that auto-deploy costs a very silly $1/month – or rather, that silly $1 needs to go away entirely</li>
  <li>A build command is required by the form, even though it’s not needed in many scenarios</li>
  <li>All names need to be at least four characters long – I get upset when I can’t name my things “foo”</li>
  <li>Healthcheck claims to be TCP, when it’s actually HTTP</li>
  <li>Healthcheck interval has a vague definition – description: “interval can’t be less than the timeout”, yet the error message says “must be greater than timeout”</li>
  <li>Healthcheck timeout has some funkyness too – my test application does <code class="language-plaintext highlighter-rouge">sleep(2)</code>, but a timeout of 3 caused it to fail; setting it to 4 worked</li>
  <li>The log is displayed upside down (newest lines first)… I mean… what?</li>
  <li>When creating the service fails deployment, the log claims to roll back – to what?</li>
  <li>When creating the service fails deployment, it cannot be updated and has to be deleted</li>
  <li>An obvious no-win-situation like <code class="language-plaintext highlighter-rouge">command not found</code> when starting the container should fail immediately, but App Runner keeps trying for minutes</li>
  <li>Generally, any kind of screwed up deployment will incur a five to 20-minute penalty, often with no helpful log output whatsoever –
having to wait “forever” <em>and</em> having virtually no insight into what’s happening is highly frustrating</li>
  <li>Even better is this:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>12:05:27 [AppRunner] Service resume started.
12:05:27 [AppRunner] Service status is set to OPERATION_IN_PROGRESS.
12:22:34 [AppRunner] Service resume failed.
12:22:35 [AppRunner] Service status is set to PAUSED.
</code></pre></div>    </div>
    <p>When the application is paused, and it won’t come up healthy, you can only delete it – this means a new URL is generated and updating the custom domain’s
DNS records, potentially running into caching issues (depending on the DNS TTL). And yes, that’s all the log there is. Note the timestamps…</p>
  </li>
  <li>Updating a service that is in the <code class="language-plaintext highlighter-rouge">PAUSED</code> state will give this error: <code class="language-plaintext highlighter-rouge">Service status must be RUNNING or PAUSED to be updated</code></li>
  <li>Deployment is slooooow, even when successful</li>
  <li>Initially my application’s stdout wouldn’t show up as log for an hour or so, but later, everything worked as expected –
and I couldn’t reproduce that, so maybe it was just my fat fingers</li>
  <li>Feature request: Make the built container available in my ECR (as an export-style option)</li>
  <li>Feature request: Generate <code class="language-plaintext highlighter-rouge">apprunner.yml</code> for a manually configured service</li>
  <li>When a healthcheck suddenly fails, the container is correctly restarted – but there is no logging about that whatsoever</li>
  <li>Custom domain: Only CNAME is supported, no Alias (just give me the Hosted Zone ID…?)</li>
  <li>Custom domain: This needs an ACM-style button “this zone lives in Route53, just create the records for me”</li>
  <li>The very last log entry of a deleted service is <code class="language-plaintext highlighter-rouge">Service status is set to OPERATION_IN_PROGRESS</code> (no log output to confirm deletion).</li>
  <li>When re-creating a service with the same name, Cloudwatch metrics will from then on be labelled with the new service-id for ActiveInstances and <em>labelled with the old service-id</em> for all request metrics</li>
  <li>Apparently, even <em>pausing</em> a service can fail:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>06-19-2021 03:54 PM [AppRunner] Service pause started.
06-19-2021 03:54 PM [AppRunner] Service status is set to OPERATION_IN_PROGRESS.
06-19-2021 04:12 PM [AppRunner] Service pause failed.
</code></pre></div>    </div>
    <p>And again note the timestamps – 18 minutes and no reason given. Also, here’s the obligatory <a href="https://xkcd.com/1179/">xkcd about ISO 8601</a>.</p>
  </li>
</ul>

<h2 id="pricing">Pricing</h2>

<p>Official pricing infos <a href="https://aws.amazon.com/apprunner/pricing/">here</a>.</p>

<p>Sadly, the smallest possible configuration is 2 GB RAM / 1 vCPU, which puts the monthly cost between $10.5 and $58.50 (depending on the amount
of idle time).</p>

<p>There really need to be configurations with less RAM.</p>

<h2 id="saturday-morning-coffee-hacking">Saturday Morning Coffee Hacking</h2>

<p>And now for a small detour that happened after finishing this article:</p>

<p>If you payed close attention to App Runner’s build output, you’ll have noticed that the build command is executed twice: In the build
environment <em>and</em> as part of the Dockerfile. I can only assume that this maps to the <code class="language-plaintext highlighter-rouge">pre-build</code> and <code class="language-plaintext highlighter-rouge">build</code> commands of the
<a href="https://docs.aws.amazon.com/apprunner/latest/dg/config-file-ref.html">configuration file</a>. Anyway, this reminded me that I should
poke around in the build environment, too.</p>

<p>This is the build environment’s
<a href="https://docs.aws.amazon.com/cli/latest/reference/sts/get-caller-identity.html">STS get-caller-identity</a>:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Build] {
[Build]     "UserId": "AROA2W2P3O4YHNMOKQJK7:AWSCodeBuild-2834f7f3-ca90-4f78-b630-0183887718a0",
[Build]     "Account": "736217495344",
[Build]     "Arn": "arn:aws:sts::736217495344:assumed-role/bullet-system-build-role-6b4120addb68419590ea47b94506ed43/AWSCodeBuild-2834f7f3-ca90-4f78-b630-0183887718a0"
[Build] }
</code></pre></div></div>

<p>Some interesting environment variables:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Build] BULLET_IMAGE_REPO=736217495344.dkr.ecr.eu-west-1.amazonaws.com/image-repo-6b4120addb68419590ea47b94506ed43:latest
[Build] AWS_CONTAINER_CREDENTIALS_RELATIVE_URI=/v2/credentials/739b1a1e-ff7d-42b0-9b8c-05b07d3cc414
[Build] N_SRC_DIR=/n
[Build] CODEBUILD_BUILD_URL=https://eu-west-1.console.aws.amazon.com/codebuild/home?region=eu-west-1#/builds/Bufkpuli2wgb7b:96d122fd-7ae9-443f-b021-4a439e4a62fb/view/new
[Build] AWS_EXECUTION_ENV=AWS_ECS_EC2
[Build] BULLET_ASSETS_ACCOUNT_ID=172267607524
[Build] BULLET_AWS_ACCOUNT_ID=736217495344
[Build] CODEBUILD_SOURCE_REPO_URL=fusion-source-prod-eu-west-1-cell18/ufkpuli2wgb7b/d173d836d33a414595e5e5f316ad5e88/app
</code></pre></div></div>

<p>The ECS <code class="language-plaintext highlighter-rouge">/task</code> metadata didn’t reveal anything interesting; as far as I can tell, it’s just a standard CodeBuild environment. Though
the image’s tag caught my eye:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>      "Image": "570169269855.dkr.ecr.eu-west-1.amazonaws.com/codefactory-eu-west-1-prod-default-amazonlinux2-x86_64-standard:3.0-thirdwave"`.
</code></pre></div></div>
<p>Interesting that they are using ECS-EC2 instead of Fargate.</p>

<p>Retrieving the container credentials works too, <a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html">as documented</a>, but refuses to give me the complete credentials: <code class="language-plaintext highlighter-rouge">"SecretAccessKey":"***"</code>. As it turns out, this is just a clever filter in the log output (in Cloudwatch maybe?) – piping it
through <code class="language-plaintext highlighter-rouge">base64</code> makes it possible to liberate the full credentials and use them on any EC2 instance.</p>

<p>This allowed me to conveniently get an ECR authorization token and docker-pull the base image, as well as the image that App Runner creates for me:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[ec2-user@ip-10-0-0-230 ~]$ docker pull 172267607524.dkr.ecr.eu-west-1.amazonaws.com/awsfusionruntime-python3:3.8.5
3.8.5: Pulling from awsfusionruntime-python3
a1534962cb01: Pull complete 
30101d64442c: Pull complete 
Digest: sha256:06cffb6a687d8ca78c477e3de301b2a57650080fefa43259cdbf74308e5ce87f
Status: Downloaded newer image for 172267607524.dkr.ecr.eu-west-1.amazonaws.com/awsfusionruntime-python3:3.8.5
172267607524.dkr.ecr.eu-west-1.amazonaws.com/awsfusionruntime-python3:3.8.5
</code></pre></div></div>
<p>(same for <code class="language-plaintext highlighter-rouge">736217495344.dkr.ecr.eu-west-1.amazonaws.com/image-repo-6b4120addb68419590ea47b94506ed43</code>)</p>

<p>I couldn’t find much else I can do with this role’s credentials. Neither was it possible to pull <code class="language-plaintext highlighter-rouge">aws-fargate-request-proxy</code> nor did the role have
any permissions that it shouldn’t have, as far as I could tell from trying a few harmless things. So, while interesting, everything looks sane here.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Conceptually, App Runner is great – it’s very simple, yet fits a <em>lot</em> of simple web applications perfectly. Also the billing model is a huge
step in the right direction to reduce CPU time waste.</p>

<p>I’m really curious to see how App Runner will evolve – especially how the team will fight off feature creep. Some additions will be necessary
(VPC support for example), but keeping the service simple will be very tough. If they add everything someone might be wishing for, we’ll end up
where we started before App Runner came along.</p>

<hr />

<p><a href="https://twitter.com/apparentorder/status/1406509913199759362">Discuss and/or follow on Twitter!</a></p>

<hr />]]></content><author><name></name></author><summary type="html"><![CDATA[AWS App Runner: Overview, billing excitement and technical deep-dive]]></summary></entry><entry><title type="html">AWS: Mirroring Docker Images</title><link href="https://tty.neveragain.de/2021/05/07/codebuild-ecr-mirror.html" rel="alternate" type="text/html" title="AWS: Mirroring Docker Images" /><published>2021-05-07T00:00:00+02:00</published><updated>2021-05-07T00:00:00+02:00</updated><id>https://tty.neveragain.de/2021/05/07/codebuild-ecr-mirror</id><content type="html" xml:base="https://tty.neveragain.de/2021/05/07/codebuild-ecr-mirror.html"><![CDATA[<p>A recent discussion on the <code class="language-plaintext highlighter-rouge">##aws</code> IRC was about how people handle Docker Hub’s recently introduced
<a href="https://www.docker.com/increase-rate-limits">rate limiting</a>.</p>

<p>This post will explore a CodeBuild-based solution for periodic mirroring of images.</p>

<h3 id="background">Background</h3>

<p>Container images need to be <em>pulled</em> from somewhere. By default, that’s Docker Hub. So when I’m using
simply <code class="language-plaintext highlighter-rouge">FROM nginx</code> in my Dockerfile, that’s actually <code class="language-plaintext highlighter-rouge">docker.io/nginx</code>.</p>

<p>This is fine for an occasional build process.</p>

<p>But it’s quite problematic for automated use. For example, testing that runs at high frequency can cause
a significant amount of <em>pull</em>s. The same goes for frequent deployments or ECS tasks, or any trigger that’s
based on a public docker image. Especially problematic are misconfigured / failing services, which will
perpetually try to start a service, pulling the image each time.</p>

<p>Understandably, Docker Hub is unwilling to serve massive amounts of – basically unnecessary – pulls for free.
So I can’t really blame them for introducing those rate limits in November 2020, and I think their rate limit for free usage is reasonable. Also keep in mind that many images are rather large – sizes beyond one gigabyte are common.</p>

<p>This rate limiting becomes a significant issue on AWS though: In almost all scenarios, the pull request will be issued from an AWS IP
address that’s effectively random. There’s no way of knowing how many pulls have already been made from that address today.
Therefore, any pull from AWS has a good chance of failing with this error: <code class="language-plaintext highlighter-rouge">Error response from daemon: toomanyrequests: You have reached your pull rate limit</code>.</p>

<h3 id="other-issues">Other Issues</h3>

<p>Using a frequently-pulled image from an external source has more issues besides rate-limiting errors:</p>

<ul>
  <li>
    <p>It’s wasteful: This is a <em>lot</em> of data transfer over the public internet – ingress traffic is free on AWS, but that’s no excuse for completely avoidable significant traffic</p>
  </li>
  <li>
    <p>It’s slow: Pulling an image from the public internet takes significantly longer than using an image hosted in my AWS region.</p>
  </li>
  <li>
    <p>It’s unreliable: As with any external service, there’s always an increased chance that it’s unreachable (for network or other issues – not to mention that there’s no SLA for a free service)</p>
  </li>
</ul>

<p>Generally speaking, not pulling the same image hundreds of times is just good manners.</p>

<h3 id="workarounds">Workarounds</h3>

<p>AWS responded by publishing <a href="https://aws.amazon.com/de/blogs/containers/advice-for-customers-dealing-with-docker-hub-rate-limits-and-a-coming-soon-announcement/">advice on the AWS blog</a>.</p>

<p>Basically, the options are:</p>

<ul>
  <li>
    <p>Paying for a Docker Hub account. This might seem like the right thing to do, but doesn’t address any of the other issues.</p>
  </li>
  <li>
    <p>Using the <a href="https://aws.amazon.com/de/blogs/aws/amazon-ecr-public-a-new-public-container-registry/">AWS Public ECR</a>. AWS managed to launch this incredibly fast after Docker’s announcement, and I believe it’s the best long-term solution. But as of today, many common images are not available there. I think a marketing push would be in order…</p>
  </li>
  <li>
    <p>Manually copying the required images from Docker Hub to my AWS account’s ECR.</p>
  </li>
</ul>

<p>In most cases, using the account’s private ECR is the most reasonable path out of this mess. But now I have a classic Day 2 problem: How do I make sure
my mirrored image is up to date?</p>

<p>As with anything in life, the answer is automation.</p>

<p>Well, once again I’m writing too much. So let’s get to it.</p>

<h3 id="overview">Overview</h3>

<p>My solution will be based on these components:</p>

<ul>
  <li>
    <p>CodeBuild provides the environment to pull an image from Docker Hub and then push it to my ECR</p>
  </li>
  <li>
    <p>EventBridge to trigger CodeBuild on a fixed schedule</p>
  </li>
  <li>
    <p>ECR to store the copied images</p>
  </li>
</ul>

<p>An important goal is to easily do this for multiple images, so the CodeBuild project has to accept the image name as a parameter.</p>

<p>I will use the public images of <code class="language-plaintext highlighter-rouge">nginx</code> and <code class="language-plaintext highlighter-rouge">logstash</code> as examples.</p>

<h3 id="ecr">ECR</h3>

<p>I need to create the repositories for the images to be mirrored.</p>

<ul>
  <li>“Create repository” in the ECR Console</li>
  <li>Use <code class="language-plaintext highlighter-rouge">nginx</code> as repository name</li>
  <li>For everything else, defaults are fine</li>
  <li>Repeat for <code class="language-plaintext highlighter-rouge">logstash</code> and any others as necessary</li>
</ul>

<p>Note the <em>registry URL</em> of the private ECR repositories – something like <code class="language-plaintext highlighter-rouge">123456789012.dkr.ecr.eu-central-1.amazonaws.com</code> (without the trailing <code class="language-plaintext highlighter-rouge">/nginx</code>).</p>

<h3 id="codebuild-project">CodeBuild Project</h3>

<p>Then I’ll create the CodeBuild project that takes care of mirroring:</p>

<ul>
  <li>“Create build project” in the CodeBuild Console</li>
  <li>Use some beautiful name like <em>foo</em> or <em>ecr-mirror</em></li>
  <li>As <em>Source</em>, I select <em>No source</em></li>
  <li>Under <em>Environment</em>,
    <ul>
      <li>I’ll use any next-best-latest Linux image; currently, that is
        <ul>
          <li>Operating system: Amazon Linux 2</li>
          <li>Runtime: Standard</li>
          <li>Image: <code class="language-plaintext highlighter-rouge">aws/codebuild/amazonlinux2-x86_64-standard:3.0</code></li>
        </ul>
      </li>
      <li>Enable <em>Privileged</em></li>
      <li>Use a <em>New service role</em> (note the name!)</li>
      <li>In <em>Additional configuration</em>,
        <ul>
          <li>add an <em>Environment variable</em> named <code class="language-plaintext highlighter-rouge">REGISTRY</code>, using the <em>registry URL</em> from the ECR step</li>
          <li>leave everything else as it is</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>In <em>Buildspec</em>, I’ll <em>Switch to editor</em>, remove all suggested lines, and paste the build specification given below</li>
  <li>Finally, <em>Create build project</em></li>
</ul>

<p>Here’s the build specification to use:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>version: 0.2

phases:
  build:
    commands:
       - aws ecr get-login-password | docker login --username AWS --password-stdin $REGISTRY
       - docker pull $PREFIX$IMAGE
       - docker tag $PREFIX$IMAGE $REGISTRY/$IMAGE
       - docker push $REGISTRY/$IMAGE
</code></pre></div></div>

<p>Finally, hidden in the project’s <em>Build details</em> tab, I’ll note the <em>Project ARN</em>.</p>

<h3 id="adjust-iam-permissions-for-codebuild">Adjust IAM Permissions for CodeBuild</h3>

<p>CodeBuild created a basic IAM Role for my project, but this role is not allowed to access my ECR repositories yet. To fix that:</p>

<ul>
  <li>In the IAM Console, I locate the <em>Role</em> that has been created by CodeBuild (named as noted above; should be <code class="language-plaintext highlighter-rouge">codebuild-ecr-mirror-service-role</code>)</li>
  <li><em>Attach policies</em>: <code class="language-plaintext highlighter-rouge">AmazonEC2ContainerRegistryPowerUser</code></li>
</ul>

<h3 id="eventbridge-rule">EventBridge Rule</h3>

<p>I’ll use EventBridge to periodically update the images.</p>

<ul>
  <li>In the EventBridge Console, <em>Create rule</em></li>
  <li>name it <em>ecr-mirror</em></li>
  <li>As pattern, I’ll select <em>Schedule</em></li>
  <li>I could use a <em>fixed rate</em>, but to have better control, I’ll use a <em>Cron expression</em>: <code class="language-plaintext highlighter-rouge">10 0 * * ? *</code> (that’s 00:10 UTC every day)</li>
  <li>As <em>Target</em>, I select a <em>CodeBuild project</em> and use the <em>Project ARN</em> from above</li>
  <li>Expand <em>Configure input</em>, and there
    <ul>
      <li>select <em>Constant (JSON text)</em></li>
      <li>paste the JSON given below (for nginx)</li>
    </ul>
  </li>
  <li><em>Add target</em> (for logstash)
    <ul>
      <li>using the same CodeBuild target and ARN</li>
      <li>using the same JSON text, but replacing <code class="language-plaintext highlighter-rouge">nginx:stable</code> with <code class="language-plaintext highlighter-rouge">logstash:latest</code></li>
    </ul>
  </li>
  <li>Leave the rest as is and <em>Create</em></li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
	"environmentVariablesOverride": [
		{
			"name": "IMAGE",
			"type": "PLAINTEXT",
			"value": "nginx:stable"
		}
	]
}
</code></pre></div></div>

<p>It will be a single line after pasting it – that’s fine.</p>

<h3 id="testing">Testing</h3>

<p>Scheduled jobs are annoying to test. I could adjust the <em>Cron specification</em> to trigger a few minutes from now.</p>

<p>A faster (but less thorough) way to test is the <em>Start build with overrides</em> button in the CodeBuild project overview.
There under <em>Environment variables override</em>, I’ll <em>Add environment variable</em> named <code class="language-plaintext highlighter-rouge">IMAGE</code> with a value of, for example, <code class="language-plaintext highlighter-rouge">logstash:7.11.2</code>.</p>

<p>In the resulting build process, I like to click <em>Tail logs</em>.</p>

<p>By the way, CodeBuild will log to CloudWatch by default (in the <code class="language-plaintext highlighter-rouge">/aws/codebuild/ecr-mirror</code> log group, unless changed).</p>

<h3 id="next-steps">Next Steps</h3>

<p>I now have a mirror of the images I need, updated daily.</p>

<p>What’s left is changing all references to the image, e.g. in my ECS task definitions and Dockerfiles. Instead of <code class="language-plaintext highlighter-rouge">nginx</code>, I’ll change those to use <code class="language-plaintext highlighter-rouge">123456789012.dkr.ecr.eu-central-1.amazonaws.com/nginx</code>. This requires a login to that registry first, of course. <a href="https://docs.aws.amazon.com/AmazonECR/latest/userguide/ECR_on_ECS.html">ECS handles this</a> transparently, but it needs the appropriate IAM permissions. Same for <a href="https://docs.aws.amazon.com/AmazonECR/latest/userguide/ECR_on_EKS.html">EKS</a>.</p>

<p>Further improvements could be made:</p>

<p>For one, narrowing down CodeBuild’s IAM permissions to the appropriate repositories might be a good idea.</p>

<p>And for mirroring more than a few images, the CodeBuild job could be adjusted to retrieve the list of images from somewhere else, for example a <em>StringList</em> in the SSM Parameter Store.</p>

<h3 id="the-docker-in-the-room">The Docker in the Room</h3>

<p>You might have noticed that this CodeBuild solution is plagued by the same problem initially described here: There’s a good chance that it will fail because of Docker Hub’s rate limiting.</p>

<p>Honestly, I don’t have a good idea for that. Most of the time it works. When it does not, a manual <em>Retry build</em> usually helps. So yes, a daily update could fail for one or two days – but usually, that’s not an issue. And if it is, you can always intervene manually and/or increase the mirroring frequency.</p>

<p>All in all, the whole situation is a mess without a good solution. But this one is better than nothing / mirroring by hand.</p>

<p>Let me know how you handle images!</p>

<hr />

<p><a href="https://twitter.com/apparentorder/status/1390689497206173699">Discuss and/or follow on Twitter!</a></p>

<hr />]]></content><author><name></name></author><summary type="html"><![CDATA[Using CodeBuild and EventBridge to maintain an up-to-date mirror of public container images]]></summary></entry><entry><title type="html">AWS Billing: A Leap of Faith</title><link href="https://tty.neveragain.de/2021/04/27/aws-billing-leap-of-faith.html" rel="alternate" type="text/html" title="AWS Billing: A Leap of Faith" /><published>2021-04-27T00:00:00+02:00</published><updated>2021-04-27T00:00:00+02:00</updated><id>https://tty.neveragain.de/2021/04/27/aws-billing-leap-of-faith</id><content type="html" xml:base="https://tty.neveragain.de/2021/04/27/aws-billing-leap-of-faith.html"><![CDATA[<hr />

<p><em>This post is part of a blog series: <a href="/2021/04/26/road-to-reweb.html">Road to re:Web</a>.</em></p>

<hr />

<p>The AWS sign-up is a pretty scary experience for individuals without prior AWS experience.</p>

<p>But it’s an experience that became a motivation for me.</p>

<h3 id="a-leap-of-faith">A Leap of Faith</h3>

<p>For individual users, <em>every</em> dollar counts. Every dollar I save is a dollar I got. This is different from companies, where
expected savings must be significant to justify expensive humans spending time on cost optimization.</p>

<p>And individual users isn’t just hobbyists – it’s students, freelancers, FOSS developers, small non-profits, people eager to
“learn Cloud” and many others.</p>

<p>As an individual user, signing up for AWS is scary. I remember that vividly. It took me weeks to get over that paranoia.
That’s <em>my</em> credit card, dammit. If I misunderstand something, or I forget to turn something off, or I don’t pay attention and
click some super-expensive option instead of the covered-by-free-tier option, it’s <em>my</em> ass that gets fucked – this can go into
four-digits territory fascinatingly fast. This gets less intimidating quickly with some AWS experience, but that’s kinda Catch 22
for completely novice users.</p>

<p>And it’s not unjustified fear: There’s a lot of such horror stories out there. To be fair, most of them ended well, because AWS
seems to be <em>very</em> understanding with rookie mistakes. This is good to hear, but a far cry from any guarantee that I won’t be broke
tomorrow.</p>

<p>In the end, I took a leap of faith and signed up.</p>

<h3 id="cost-optimization-as-a-mindset">Cost Optimization as a Mindset</h3>

<p>So yeah, the whole sign-up billing fear is off-putting – and I bet AWS is losing tons of future power users and decision
makers this way.</p>

<p>I also bet a <em>lot</em> of services are never tried because it just feels too risky to an AWS novice, or the pricing information is just
too confusing.</p>

<p>But the experience has taught me a very valuable lesson, once I made the jump: It forced me to go on a first-name basis with AWS
billing.  It makes me think about costs constantly. I’m always on high alert when I’m trying new things on my personal AWS account.
I carefully read the pricing information of services. I check that Billing Dashboard religiously. That is, of course, in addition to
the recommended safeguards like <a href="https://aws.amazon.com/blogs/aws-cost-management/getting-started-with-aws-budgets/">configuring
Budgets</a>.</p>

<p>I believe most AWS users act differently if they haven’t been through the experience themselves – if it’s not their own credit card
that’s backing the charges. It’s just like security – either you actually care and consider this in your decisions, or you do not care.
Neither security nor cost-awareness can be bolted onto projects easily once they’re “done”.</p>

<h3 id="-and-as-a-motivation">… and as a Motivation</h3>

<p>With my personal account, I’m a hobbyist who has some <a href="https://twitter.com/apparentorder/status/1383323344070533126">nonsense
projects</a> running on AWS.</p>

<p>But designing and building things on AWS for customers is also my day job, so costs are always a topic.</p>

<p>It’s kinda weird, but cost optimization has now become a bit of a game for me. It’s a complex issue to think about, and what we
would have considered a good and cost-efficient architecture back in the Olden Days can have completely different cost
characteristics on AWS, especially with changing usage patterns.</p>

<p>It’s a fun challenge, and it’s part of my motivation to push serverless architectures with <a href="https://reweb.neveragain.de/info/">re:Web</a>.</p>

<p>So that’s the reward for this leap of faith, I guess.</p>

<hr />

<p><a href="https://twitter.com/apparentorder/status/1388126452395220994">Discuss and/or follow on Twitter!</a></p>

<hr />

<p><em>This post is part of a blog series: <a href="/2021/04/26/road-to-reweb.html">Road to re:Web</a>.</em></p>

<hr />]]></content><author><name></name></author><summary type="html"><![CDATA[AWS Billing is scary, and a significant barrier to entry for individuals]]></summary></entry><entry><title type="html">Road to re:Web</title><link href="https://tty.neveragain.de/2021/04/26/road-to-reweb.html" rel="alternate" type="text/html" title="Road to re:Web" /><published>2021-04-26T00:00:00+02:00</published><updated>2021-04-26T00:00:00+02:00</updated><id>https://tty.neveragain.de/2021/04/26/road-to-reweb</id><content type="html" xml:base="https://tty.neveragain.de/2021/04/26/road-to-reweb.html"><![CDATA[<p><a href="https://reweb.neveragain.de/info/">re:Web</a> is a little side-project that enables classic
web applications to run on AWS Lambda.</p>

<p>The road that led me there shall henceforth be remembered through a series of blog posts.</p>

<p>This very post will service as table of contents, if you will.</p>

<ul>
  <li>
    <p>Part 1: <a href="/2021/04/27/aws-billing-leap-of-faith.html">AWS Billing: A Leap of Faith</a></p>
  </li>
  <li>
    <p>Part 2: <a href="/2021/06/29/timeless-services.html">Timeless Services</a></p>
  </li>
  <li>
    <p>Part 3: Amazon Timestream and the Grafana use-case</p>
  </li>
  <li>
    <p>Part 4: The Epiphany of Lambda Container Image Support</p>
  </li>
  <li>
    <p>Part 5: Lambda Deep Dive: Architecture, Runtime API</p>
  </li>
  <li>
    <p>Part 6: Lambda Deep Dive: Connecting to ALB / API Gateway</p>
  </li>
  <li>
    <p>Part 7: Putting It All Together</p>
  </li>
  <li>
    <p>Part 8: Packaging / Distribution Insane Asylum</p>
  </li>
</ul>

<p>This post will be updated as I complete the individual blog posts. Culmination can be
expected in about two dozen months. Give or take.</p>

<p>The above list is highly likely to be subject to change.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Introduction / Overview post for the Road to re:Web series of blog entries]]></summary></entry></feed>