[{"content":"Originally published on DEV Community.\nI run a decent little stack of services out of my house. Whisper (an end-to-end encrypted secret sharing app), bar_keep, archivist, a few other side projects, plus the base infra that ties them together: Traefik, shared networks, a handful of private stacks. For a while every deploy meant SSHing into the server, and I was pretty tired of it.\nWhat I actually wanted was something like the popular GitOps tools a lot of folks run on top of Kubernetes. Push code, get a deploy, done. No shell, no secrets on disk, no kubectl. But I\u0026rsquo;m not running Kubernetes at home and I wasn\u0026rsquo;t about to install it just for this, so I took a different route.\nI ended up gluing together three things that basically give me the same experience those tools give you, just lighter and totally free on top of GitHub. It\u0026rsquo;s all stuff I already run, the control plane is GitHub itself, and honestly I think it\u0026rsquo;s a pretty fun pattern that other folks with home servers might like.\nThe three pieces There are three parts to this, and two of them are tools I built:\ngithub-multi-runner — a single container that runs a bunch of GitHub Actions self-hosted runners, configured via a JSON file. docker-compose-deploy — a composite GitHub Action that runs docker compose up on a self-hosted runner. The pattern: a private \u0026ldquo;base infra\u0026rdquo; repo that owns networks, volumes, and shared stacks, plus each service repo owning its own real compose.yml that is both the deployment config and a working example for anyone reading the repo. The runner container is the thing that turns \u0026ldquo;my home server\u0026rdquo; into \u0026ldquo;a GitHub Actions target.\u0026rdquo; The action is the thing that actually deploys. The pattern is what ties it all together so I never have to touch a terminal to ship.\ngithub-multi-runner: one container, many runners The official ghcr.io/actions/actions-runner image is designed around one runner per container. Which is fine, but I\u0026rsquo;ve got a bunch of repos (public and private) plus an org, and spinning up 10 containers just to attach to 10 scopes feels silly. I also wanted to be able to add and remove runners without bouncing anything else.\nSo github-multi-runner is just a bash entrypoint and a JSON file mounted into the official image. No custom image to build and maintain. The JSON looks like this:\n{ \u0026#34;runners\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;my-org\u0026#34;, \u0026#34;scope\u0026#34;: \u0026#34;org\u0026#34;, \u0026#34;target\u0026#34;: \u0026#34;my-github-org\u0026#34; }, { \u0026#34;name\u0026#34;: \u0026#34;my-repo\u0026#34;, \u0026#34;scope\u0026#34;: \u0026#34;repo\u0026#34;, \u0026#34;target\u0026#34;: \u0026#34;myuser/my-repo\u0026#34; }, { \u0026#34;name\u0026#34;: \u0026#34;whisper\u0026#34;, \u0026#34;scope\u0026#34;: \u0026#34;repo\u0026#34;, \u0026#34;target\u0026#34;: \u0026#34;nckslvrmn/whisper\u0026#34; } ] } The entrypoint watches that file. If you add a runner, it registers and starts it. If you change one, it gracefully deregisters and re-registers just that one. If you remove a runner, it drains it. Unrelated runners are never touched. It also handles docker socket access automatically (detects the GID and adds the runner user to a matching group), so workflows that run docker compose just work.\nA few things I cared about while building it: graceful drains so nothing gets yanked mid-job, persistent registrations across container restarts so there\u0026rsquo;s no deregister/re-register thrash, and per-runner log files so debugging is just a tail -F away.\nThe whole thing is a single bash script because bash is what ships in the runner image and I didn\u0026rsquo;t want another dependency.\nThe deploy on the host side is just a compose file with the official runner image, the entrypoint mounted in, the JSON config mounted in, the docker socket mounted in, and a GITHUB_TOKEN in the environment. That\u0026rsquo;s it.\ndocker-compose-deploy: the dumbest possible deploy action This one is even simpler. It\u0026rsquo;s a composite GitHub Action whose entire job is:\n- uses: docker/setup-compose-action@v2 - shell: bash run: docker compose -f \u0026#34;${{ inputs.file }}\u0026#34; up -d --pull always --remove-orphans That\u0026rsquo;s really it. It takes a file path and optional args and runs docker compose up. It\u0026rsquo;s not magic. The magic is where it runs, which is on a self-hosted runner on my server, which means docker compose up happens on the actual deploy target.\nBecause it\u0026rsquo;s a normal GitHub Action, I get all the things that come with that for free:\nSecrets from GitHub\u0026rsquo;s encrypted secret store get injected as env vars on the step, and docker compose substitutes them into the compose file at runtime. Secrets never land in the compose file or on disk. The workflow log is the deploy log. If the compose file is bad, the workflow fails and I get an email. workflow_dispatch gives me a big green \u0026ldquo;deploy\u0026rdquo; button in the GitHub UI for manual deploys. The pattern: real compose files in service repos Here\u0026rsquo;s the part I think is actually the coolest bit. It\u0026rsquo;s not a tool, it\u0026rsquo;s just a convention.\nEvery service repo has a real compose.yml in the root. Not an example, not a template. The literal file that gets used in production. For Whisper, it looks roughly like this:\nservices: whisper: image: ghcr.io/nckslvrmn/whisper:${WHISPER_VERSION:-latest} environment: - S3_BUCKET=${S3_BUCKET} - DYNAMO_TABLE=${DYNAMO_TABLE} volumes: - /home/nsilverman/.aws:/root/.aws/:ro labels: - \u0026#34;traefik.enable=true\u0026#34; - \u0026#34;traefik.http.routers.whisper.rule=Host(`whisper.slvr.io`)\u0026#34; - \u0026#34;traefik.http.routers.whisper.entrypoints=websecure\u0026#34; networks: - traefik networks: traefik: external: true Anyone cloning the repo to self-host Whisper gets a totally usable compose file to start from. Someone reading the repo to understand how the thing is deployed gets the literal answer. And I get to use the same file to deploy my own instance. One file, three audiences, no drift.\nThe base infra that this compose file depends on (the traefik network, the Traefik container itself, shared volumes, private stacks that don\u0026rsquo;t belong in public repos) lives in a separate private repo. That repo has its own compose file and its own deploy workflow. Everything connects via external networks, which means each service repo and the base infra repo all deploy fully independently.\nPutting it all together: Whisper as an example Whisper\u0026rsquo;s deploy workflow has two jobs. The first runs in GitHub Actions\u0026rsquo; default runner environment and builds the Docker image and pushes it to GHCR. The second runs on [self-hosted] and does the actual deploy:\ndeploy: needs: push-whisper-image runs-on: [self-hosted] steps: - uses: actions/checkout@v5 - uses: nckslvrmn/docker-compose-deploy@main with: file: compose.yml env: S3_BUCKET: ${{ secrets.S3_BUCKET }} DYNAMO_TABLE: ${{ secrets.DYNAMO_TABLE }} WHISPER_VERSION: ${{ github.ref_name }} That runs-on: [self-hosted] is the whole trick. It tells GitHub to route the job to one of my runners, which are running inside my multi-runner container on my home server. The action checks out the repo, runs docker compose up with compose.yml and the version tag from the release, and because compose does an image pull with --pull always, the new version lands. Traefik picks up the label changes automatically. Old container goes down, new one comes up, no downtime.\nWhen I cut a new tagged release on GitHub, the whole chain runs:\nRelease published. Image builds in GitHub Actions\u0026rsquo; default runner environment and gets pushed to GHCR. Deploy job runs on my home server, pulls the new image, runs compose. Profit. There\u0026rsquo;s also a workflow_dispatch variant for manual deploys where I can pass a version string in the UI. Super handy for rolling back.\nWhy I like this A few reasons:\nNo SSH, no kubectl, no shell. The entire deployment surface is a GitHub Action workflow file. If I want to deploy something new, I write a compose file and a workflow. If I want to roll back, I just rerun the deploy action with the previous version tag. No control plane to run. The popular GitOps tools need a Kubernetes cluster, a bunch of CRDs, and a UI server to actually host them. This setup needs a container and a JSON file, and GitHub is the control plane. Secrets are already solved. GitHub already has an encrypted secret store with fine-grained access controls. I don\u0026rsquo;t need Vault for a home setup. I just put secrets in the repo\u0026rsquo;s secret settings and reference them in the workflow. The compose file is the source of truth and a working example. Anyone reading the repo can see exactly how the thing is deployed. There\u0026rsquo;s no \u0026ldquo;well the real config is in some private ops repo\u0026rdquo; split. Each repo deploys on its own. No monorepo, no shared deploy pipeline, no coordination. The only shared thing is the external Traefik network and the base infra repo that owns it. It\u0026rsquo;s fully GitOps-ish. The state of my server is described by the compose files in my repos. If I nuked the server and restored the base infra repo, then re-ran every service workflow, everything would come back up in a known state. Tradeoffs, because there are always tradeoffs I\u0026rsquo;m not trying to pretend this is a full-blown GitOps platform. A few things it doesn\u0026rsquo;t do:\nSelf-hosted runners on public repos are a real security concern, but it can be done safely. Out of the box, anyone can open a PR and execute code on your host. You can absolutely run this setup on public repos if you\u0026rsquo;re careful about what triggers the self-hosted job. I only gate the deploy workflow on releases being cut, which can only be done by repo owners, so random PRs can\u0026rsquo;t touch the host. For an extra layer you can also use GitHub environment protection rules to require approval before a self-hosted job runs. This makes it pretty safe, but it\u0026rsquo;s still worth being careful about. No drift detection. If I SSH in and manually change something (which, with this setup, I basically never do anymore), nothing notices. A real continuously-reconciling controller would flag it. Here, the next deploy would just overwrite the drift. No multi-node. This is one home server. If I wanted HA across multiple machines I\u0026rsquo;d need something heavier. For a homelab this is fine. Still technically pull based via docker compose up. Images are pulled from GHCR, but the trigger for the deploy is a workflow, not a continuously-reconciling controller. If GHCR has a new image and no workflow ran, nothing happens. You can bolt on: registry_package onto the workflow to trigger on image publish, which I do in a couple spots. None of these have bitten me in practice for a home setup, but they\u0026rsquo;re worth knowing if you want to copy this.\nTry it If any of this sounds useful:\ngithub-multi-runner — drop in the compose file, point at a JSON config, set a PAT, done. docker-compose-deploy — add it as a step in any workflow that targets a self-hosted runner. whisper — has the full worked example of the release → build → deploy chain in .github/workflows/docker.yml. The whole thing clicks together in an afternoon. If you\u0026rsquo;ve got a home server and you\u0026rsquo;ve been wishing for a lighter-weight GitOps story, I think this is a pretty good one.\n","permalink":"https://nckslvr.mn/posts/home-server-gitops-lite-on-nothing-but-github-and-docker/","summary":"\u003cp\u003e\u003cem\u003eOriginally published on \u003ca href=\"https://dev.to/nckslvrmn/home-server-gitops-lite-on-nothing-but-github-and-docker-19lo\"\u003eDEV Community\u003c/a\u003e.\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eI run a decent little stack of services out of my house. Whisper (an end-to-end encrypted secret sharing app), bar_keep, archivist, a few other side projects, plus the base infra that ties them together: Traefik, shared networks, a handful of private stacks. For a while every deploy meant SSHing into the server, and I was pretty tired of it.\u003c/p\u003e\n\u003cp\u003eWhat I actually wanted was something like the popular GitOps tools a lot of folks run on top of Kubernetes. Push code, get a deploy, done. No shell, no secrets on disk, no kubectl. But I\u0026rsquo;m not running Kubernetes at home and I wasn\u0026rsquo;t about to install it just for this, so I took a different route.\u003c/p\u003e","title":"Home Server Gitops Lite on Nothing but Github and Docker"},{"content":"Originally published on Let\u0026rsquo;s Encrypt.\nNick Silverman is a Senior Infrastructure Engineer on the Edge Infrastructure team at Shopify, where he maintains the systems that provision, renew, and publish SSL certificates for millions of merchants’ custom domains. He is also a contributor to the Ruby acme-client gem.\nThe challenge Shopify’s automated certificate management system relied on a static renewal threshold: 30 days before the end of the 90-day lifetime. To spread the load of provisioning and renewing certificates, we implemented a random 0–72 hour delay for each. While this helps evenly distribute certificate management over time, it did not take into account the Certificate Authority’s (CA) load. It was also incapable of reacting to a dynamic renewal window based on information provided by the CA.\nHowever, this approach needed greater resilience to solve what is, in the end, a distributed coordination problem. The weaknesses are:\nNo rapid revocation response: The static logic is not aware of revocations at all. Brittleness to lifetime changes: The static 30-day threshold is not resilient to changes in certificate lifetime, such as Let’s Encrypt’s announced plan to move to 45-day certificates. Imperfect load distribution: Despite the random jitter, massive renewal bursts could still occur. Shopify needed to develop a global coordination system to balance the load and handle regular and urgent renewals. Thankfully, Let’s Encrypt has led the charge on a solution for this and other very important aspects of the certificate lifecycle.\nThe journey Let’s Encrypt and the Internet Engineering Task Force (IETF) published the ACME Renewal Information (ARI) standard which makes an endpoint available that provides a recommended window of time for the renewal to occur. The endpoint returns a payload that looks something like this:\nGET /renewal-info/ACME_KEY_IDENTIFIER { \u0026#34;suggestedWindow\u0026#34;: { \u0026#34;start\u0026#34;: \u0026#34;2026-02-03T04:00:00Z\u0026#34;, \u0026#34;end\u0026#34;: \u0026#34;2026-02-04T04:00:00Z\u0026#34; } } Shopify’s certificate management system uses the acme-client Ruby gem originally authored by another Shopify employee. A growing number of ACME clients, including certbot, have enabled support for ARI, but the Ruby gem did not yet support this feature. Rather than building a custom solution, we decided to enable support for the ARI extension directly in the client.\nLet’s Encrypt’s guide to integrating ARI provided the necessary roadmap, and the implementation was completed with one PR. This contribution means that not only Shopify, but also the wider Ruby community, can benefit from the ARI extension.\nDeployment and ARI at scale Once we shipped the gem support, integrating ARI into our certificate management system was straightforward. Instead of checking a static 30-day threshold, we now query the ARI endpoint and use the suggested renewal window as the gate for initiating renewals. Those dates are stored alongside the certificate upon its initial provisioning.\nThe updated Ruby gem provides a method for fetching renewal information:\nrenewal_info = client.renewal_info(certificate: existing_certificate_pem) This method generates an ARI certificate identifier that can be used when making the API call. The client also includes a helper method, suggested_renewal_time, which chooses a random time between the returned start and end dates. The certificate identifier can be passed to the new_order method via the replaces key, which can grant a higher priority or bypass rate limits for renewals occurring during the window, depending on the CA’s policies.\nCritically, Shopify also regularly polls the ARI endpoint for updated renewal timestamps. This allows our systems to rely on those timestamps as the primary renewal timing logic and removes the need for inflexible hard-coded expiry thresholds. This becomes the mechanism that Let’s Encrypt uses to dynamically change the renewal time due to a revocation event.\nResults and rewards Since enabling the use of the ARI extension, our certificate management system has become significantly more robust. Shopify now delegates the responsibility of determining renewal timing to Let’s Encrypt. The ARI extension has proven to be an impactful infrastructure improvement and the benefits gained are immediate. These benefits, alongside fewer manual interventions, are the operational success story:\nFuture-proofing: We gained resilience against any future certificate lifetime changes and mass revocation events without needing code updates, ensuring our renewal logic is flexible. Optimized load: We directly benefit from the CA’s coordinated load balancing provided by the suggested renewal window, eliminating local randomness issues and the need for complex global coordination. Revocation readiness: ARI allows systems to quickly detect and respond to revocation events when an urgent renewal is necessary, well before certificates get close to their due dates. Simple implementation: The extension is mature (RFC 9773) and the implementation is straightforward, providing simplified renewal logic and CA-optimized timing. Good citizenship: Anyone using ARI helps the CA optimize its infrastructure, and contributes to better aggregate behavior across the entire ecosystem. If you’re still relying on static renewal thresholds, give ARI a look. Shopify wholeheartedly encourages all ACME users and client developers to adopt the ARI extension.\n","permalink":"https://nckslvr.mn/posts/simplifying-certificate-renewals-with-ari/","summary":"\u003cp\u003e\u003cem\u003eOriginally published on \u003ca href=\"https://letsencrypt.org/2026/03/17/acme-renewal-information-ari/\"\u003eLet\u0026rsquo;s Encrypt\u003c/a\u003e.\u003c/em\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eNick Silverman is a Senior Infrastructure Engineer on the Edge Infrastructure team at Shopify, where he maintains the systems that provision, renew, and publish SSL certificates for millions of merchants’ custom domains. He is also a contributor to the Ruby acme-client gem.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"the-challenge\"\u003eThe challenge\u003c/h2\u003e\n\u003cp\u003eShopify’s automated certificate management system relied on a static renewal threshold: 30 days before the end of the 90-day lifetime. To spread the load of provisioning and renewing certificates, we implemented a random 0–72 hour delay for each. While this helps evenly distribute certificate management over time, it did not take into account the Certificate Authority’s (CA) load. It was also incapable of reacting to a dynamic renewal window based on information provided by the CA.\u003c/p\u003e","title":"Simplifying Certificate Renewals for Millions of Domains with ACME Renewal Information (ARI)"},{"content":"Originally published on AWS for Industries.\nBy Nick Silverman, Jigna Gandhi, Kim Fushimi, and Randy Moy.\nContainerization becoming popular for application lifecycle In the retail industry, more and more developers are using containerization as the primary software lifecycle for applications and services. There are countless benefits to this approach, and it shifts the focus of configuration and runtime management to the container itself. This means that there’s much less configuration and setup at the host level. However, you still need to consider patching and security at the host.\nThis blog will describe how Rue Gilt Groupe (RGG), a premier off-price ecommerce company comprised of Rue La La, Gilt, Gilt City, and Shop Premium Outlets, implemented an automated solution to patch and replace hosts that run the company’s containerized applications with zero downtime and minimal impact to the containers running within clusters.\nInfrastructure and setup Before we dive into how we implemented the automated patching, let’s discuss the underlying architecture that uses Amazon Web Services (AWS). With Amazon Elastic Container Service (Amazon ECS), a fully managed container orchestration service, customers can determine the optimal compute level for their containers to run in a cluster. In an Amazon ECS cluster, you can use different types of instances from Amazon Elastic Compute Cloud (Amazon EC2), which has secure and resizable compute capacity for virtually any workload, and/or AWS Fargate, a serverless, pay-as-you-go compute engine, to serve the container use cases running in the cluster. For users that choose Amazon EC2 as the underlying compute for their clusters, we recommend managing the instances within a group in Amazon EC2 Auto Scaling, where you can add or remove compute capacity to meet changes in demand, because managing cluster autoscaling on your own is difficult; you need to carefully monitor your compute capacity, so that you scale up and down at precise times to meet demand fluctuations. Instead, using Amazon ECS capacity providers and cluster autoscaling, Amazon ECS will scale the underlying Amazon EC2 instances to meet the capacity of the desired task counts. With capacity providers, you can scale to a reservation percentage target and queue containers for launch when additional capacity becomes available. We will use both technologies to automate the instance replacement process. Below is a sample architecture using the best practices and guidelines that we have described.\nPre-requisites An ECS Cluster AWS CLI installed with appropriate permissions if you want to execute each step from command line Docker environment In this example, we have three Amazon EC2 Auto Scaling groups that provide hosts to the cluster. Amazon ECS can use these clusters to run containers. Each one uses a different instance type and operating system, which can handle different types of workloads.\nYou can configure Amazon ECS services to use specific capacity providers based on the different capacity provider strategy options. We highly recommend configuring container health checks by attaching to a target group that has its own health checks or by configuring the health checks in the task definition itself.\nManaging hosts through Amazon EC2 Auto Scaling groups offers many benefits. To automate instance replacements as a strategy for patching, you can update an Amazon Machine Image (AMI) ID in your launch configuration (or launch template), and the Amazon EC2 Auto Scaling group will launch the new instances with the new AMI ID. Because AWS regularly updates AMIs with security patches and software updates, you can update the ASG’s launch template with a new version of the same AMI and then replace all of the instances running in that Amazon EC2 Auto Scaling group.\nAutomating Amazon EC2 Instance replacement Now that we’ve explained the general infrastructure and configurations that make up an Amazon ECS cluster, we can use the infrastructure to implement an automated host replacement process. We explain the step-by-step process below. It relies heavily on AWS APIs built into both the Amazon ECS and Amazon EC2 Auto Scaling group services.\nFor a given Amazon ECS cluster, implement the aws ecs list-container-instances command and use the returned list of container instance ARNs as input to the aws ecs describe-container-instances command. You should store the instance ID, instance ARN, and running task count for each instance.\nThen, implement the aws ecs describe-cluster command and use the returned list of capacity providers as input to the aws ecs describe-capacity-providers command. Store the Amazon EC2 Auto Scaling group name associated with each capacity provider.\nThen, for each stored Amazon EC2 Auto Scaling group, implement the aws autoscaling describe-auto-scaling-groups command. You’ll need information about the current launch template attached to the Amazon EC2 Auto Scaling group. Once you have that, you can implement the aws ec2 describe-launch-template-versions to get the current AMI in use. Finally, you can use the aws ec2 describe-images command to retrieve the platform and architecture of the AMI.\nUsing a preconfigured map of AMI name patterns (or SSM parameters with AMI IDs), you can look up the latest AMI matching the current AMI and create a new version of the existing launch template with this new AMI version using the aws ec2 create-launch-template-version command. We use the tags on the instances that were set by the Auto Scaling group to match an AMI from the map. From there, you can set the new version as the launch template default version, which the Amazon EC2 Auto Scaling group will then use.\nOnce that is complete, you can replace the hosts one by one using the commands we gathered in step 1.\na. First, you’ll detach the instance from its Amazon EC2 Auto Scaling group using the aws autoscaling detach-instances. This allows the Amazon EC2 Auto Scaling group to replace the instance, but it does not yet remove it from the Amazon ECS cluster. Then, you can do the rest of the work while the Amazon EC2 Auto Scaling group replaces this host.\nb. Then, you implement the aws ecs update-container-instances-state and set the state to draining. This begins the process of replacing any containers that are currently running on the draining host.\nc. You will then wait for the host to completely drain by querying to find out how many tasks are still running. When an Amazon ECS and Amazon EC2 host gets put into a draining state:\nThe scheduler will no longer schedule tasks on that host. The scheduler will gracefully stop the tasks on the host. For services, the scheduler will meet the desired state of the service and count of tasks required by that service. This means that it will reschedule tasks from the draining host to another host based on the deployment configuration parameters. For standalone ad-hoc type tasks, the scheduler will wait until the tasks are complete and they exit on their own. d. This will take a while for a few reasons:\nThere is a deregistration delay, which is configurable on containers, attached to target groups. It takes time for new containers to get up and running in a healthy state. This is a safety mechanism so that you don’t kill containers until others take their place. If nonservice containers are running, the host will simply wait until those containers stop and complete before moving on. e. Finally, once the instance has drained and is no longer running containers, you can safely close out the instance using the aws ec2 terminate-instance command.\nYou should repeat steps 3–5 for each Amazon EC2 Auto Scaling group configured as a capacity provider in the given Amazon ECS cluster. You will replace every host inside of each Amazon EC2 Auto Scaling group. But be sure to wait patiently until all containers are replaced safely. Overall, this can take a long time depending on your number of hosts and containers and how long it takes to deregister old containers and run new, healthy ones.\nReference code Here is the reference code for the process to automatically replace Amazon EC2 instances. This implementation uses Python and Boto3 and runs inside a Docker container. This can run anywhere, but you should not run it on a cluster instance that will be replaced as part of this automation because the process might never move past the host on which it’s running. However, this kind of container is a great use case for AWS Fargate because it can run within the same cluster on which the script is running.\nSource code ecs.py\nclass ECS: def __init__(self, name, boto_session, boto_config): self.boto = boto_session.client( \u0026#39;ecs\u0026#39;, config=boto_config ) self.name = name self.__cluster_instances() self.__cluster_asgs() def __cluster_instances(self): cluster_instances = [] resp = self.boto.list_container_instances( cluster=self.name ) instances = self.boto.describe_container_instances( cluster=self.name, containerInstances=resp[\u0026#39;containerInstanceArns\u0026#39;] ) for instance in instances[\u0026#39;containerInstances\u0026#39;]: cluster_instances.append({ \u0026#39;instance_id\u0026#39;: instance[\u0026#39;ec2InstanceId\u0026#39;], \u0026#39;arn\u0026#39;: instance[\u0026#39;containerInstanceArn\u0026#39;], \u0026#39;running_count\u0026#39;: instance[\u0026#39;runningTasksCount\u0026#39;] }) self.cluster_instances = sorted(cluster_instances, key=lambda k: k[\u0026#39;running_count\u0026#39;]) def __cluster_asgs(self): self.cluster_asgs = [] resp = self.boto.describe_clusters( clusters=[self.name] ) capacity_providers = self.boto.describe_capacity_providers( capacityProviders=resp[\u0026#39;clusters\u0026#39;][0][\u0026#39;capacityProviders\u0026#39;] ) for provider in capacity_providers[\u0026#39;capacityProviders\u0026#39;]: if provider[\u0026#39;status\u0026#39;] == \u0026#39;ACTIVE\u0026#39;: asg_name = provider[\u0026#39;autoScalingGroupProvider\u0026#39;][\u0026#39;autoScalingGroupArn\u0026#39;].split(\u0026#39;/\u0026#39;)[-1] self.cluster_asgs.append(asg_name) def drain_instance(self, instance): self.boto.update_container_instances_state( cluster=self.name, containerInstances=[instance[\u0026#39;arn\u0026#39;]], status=\u0026#39;DRAINING\u0026#39; ) def instance_task_count(self, instance): resp = self.boto.describe_container_instances( cluster=self.name, containerInstances=[instance[\u0026#39;arn\u0026#39;]] ) return resp[\u0026#39;containerInstances\u0026#39;][0][\u0026#39;runningTasksCount\u0026#39;] def deregister_instance(self, instance): self.boto.deregister_container_instance( cluster=self.name, containerInstance=instance[\u0026#39;arn\u0026#39;] ) asg.py\nclass ASG: def __init__(self, name, boto_session, boto_config): self.boto_asg = boto_session.client( \u0026#39;autoscaling\u0026#39;, config=boto_config ) self.boto_ec2 = boto_session.client( \u0026#39;ec2\u0026#39;, config=boto_config ) self.boto_ssm = boto_session.client( \u0026#39;ssm\u0026#39;, config=boto_config ) self.name = name self.__asg_info() self.__lt_curr_ami() self.__latest_ami() def __asg_info(self): resp = self.boto_asg.describe_auto_scaling_groups( AutoScalingGroupNames=[self.name] ) self.instances = [instance[\u0026#39;InstanceId\u0026#39;] for instance in resp[\u0026#39;AutoScalingGroups\u0026#39;][0][\u0026#39;Instances\u0026#39;]] if resp[\u0026#39;AutoScalingGroups\u0026#39;][0].get(\u0026#39;MixedInstancesPolicy\u0026#39;) is not None: lt = resp[\u0026#39;AutoScalingGroups\u0026#39;][0][\u0026#39;MixedInstancesPolicy\u0026#39;][\u0026#39;LaunchTemplate\u0026#39;][\u0026#39;LaunchTemplateSpecification\u0026#39;] elif resp[\u0026#39;AutoScalingGroups\u0026#39;][0].get(\u0026#39;LaunchTemplate\u0026#39;) is not None: lt = resp[\u0026#39;AutoScalingGroups\u0026#39;][0][\u0026#39;LaunchTemplate\u0026#39;] else: return None self.lt_name = lt[\u0026#39;LaunchTemplateName\u0026#39;] self.lt_version = lt[\u0026#39;Version\u0026#39;] self.orig_desired = resp[\u0026#39;AutoScalingGroups\u0026#39;][0][\u0026#39;DesiredCapacity\u0026#39;] self.os_name = next(tag[\u0026#39;Value\u0026#39;] for tag in resp[\u0026#39;AutoScalingGroups\u0026#39;][0][\u0026#39;Tags\u0026#39;] if tag[\u0026#39;Key\u0026#39;] == \u0026#39;OS\u0026#39;) def __lt_curr_ami(self): ltv = self.boto_ec2.describe_launch_template_versions( LaunchTemplateName=self.lt_name, Versions=[str(self.lt_version)] ) self.lt_curr_ami = ltv[\u0026#39;LaunchTemplateVersions\u0026#39;][0][\u0026#39;LaunchTemplateData\u0026#39;][\u0026#39;ImageId\u0026#39;] ami = self.boto_ec2.describe_images( ImageIds=[self.lt_curr_ami] ) self.platform = ami[\u0026#39;Images\u0026#39;][0][\u0026#39;PlatformDetails\u0026#39;].lower() self.architecture = ami[\u0026#39;Images\u0026#39;][0][\u0026#39;Architecture\u0026#39;].lower() def __latest_ami(self): ami_params = { \u0026#39;al2\u0026#39;: { \u0026#39;arm64\u0026#39;: \u0026#39;/aws/service/ecs/optimized-ami/amazon-linux-2/arm64/recommended/image_id\u0026#39;, \u0026#39;x86_64\u0026#39;: \u0026#39;/aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id\u0026#39; }, \u0026#39;bottlerocket\u0026#39;: { \u0026#39;arm64\u0026#39;: \u0026#39;/aws/service/bottlerocket/aws-ecs-1/arm64/latest/image_id\u0026#39;, \u0026#39;x86_64\u0026#39;: \u0026#39;/aws/service/bottlerocket/aws-ecs-1/x86_64/latest/image_id\u0026#39; }, \u0026#39;windows\u0026#39;: { \u0026#39;x86_64\u0026#39;: \u0026#39;/aws/service/ami-windows-latest/Windows_Server-2019-English-Full-ECS_Optimized/image_id\u0026#39;, } } resp = self.boto_ssm.get_parameter( Name=ami_params[self.os_name][self.architecture] ) self.latest_ami = resp[\u0026#39;Parameter\u0026#39;][\u0026#39;Value\u0026#39;] def instance_ami(self, instance_id): instance = self.boto_ec2.describe_instances( InstanceIds=[instance_id] ) return instance[\u0026#39;Reservations\u0026#39;][0][\u0026#39;Instances\u0026#39;][0][\u0026#39;ImageId\u0026#39;] def curr_capacity(self): resp = self.boto_asg.describe_auto_scaling_groups( AutoScalingGroupNames=[self.name] ) return resp[\u0026#39;AutoScalingGroups\u0026#39;][0][\u0026#39;DesiredCapacity\u0026#39;] def update_launch_template(self): resp = self.boto_ec2.create_launch_template_version( LaunchTemplateName=self.lt_name, SourceVersion=str(self.lt_version), VersionDescription=\u0026#39;Automated AMI Update\u0026#39;, LaunchTemplateData={ \u0026#39;ImageId\u0026#39;: self.latest_ami } ) self.lt_new_ver = resp[\u0026#39;LaunchTemplateVersion\u0026#39;][\u0026#39;VersionNumber\u0026#39;] def set_launch_template_version(self): self.boto_ec2.modify_launch_template( LaunchTemplateName=self.lt_name, DefaultVersion=str(self.lt_new_ver) ) def detach_instance_from_asg(self, instance_id): self.boto_asg.detach_instances( InstanceIds=[instance_id], AutoScalingGroupName=self.name, ShouldDecrementDesiredCapacity=False ) def terminate_instance(self, instance_id): self.boto_ec2.terminate_instances( InstanceIds=[instance_id] ) ecs_ami_updater\n#!/usr/bin/env python import logging import sys import time import boto3 import configargparse from botocore.config import Config from lib.asg import ASG from lib.ecs import ECS class Updater: def __init__(self): self.__parse_args() self.__init_log() self.logger.info(f\u0026#39;input arguments: {self.args.__dict__}\u0026#39;) self.boto_session = boto3.Session() self.boto_config = Config( region_name=self.args.region, signature_version=\u0026#39;v4\u0026#39; ) def __parse_args(self): parser = configargparse.ArgumentParser() parser.add_argument( \u0026#39;-c\u0026#39;, \u0026#39;--cluster\u0026#39;, env_var=\u0026#39;CLUSTER\u0026#39;, required=True, help=\u0026#39;name of cluster for which to replace instances\u0026#39; ) parser.add_argument( \u0026#39;-r\u0026#39;, \u0026#39;--region\u0026#39;, env_var=\u0026#39;AWS_REGION\u0026#39;, default=\u0026#39;us-east-1\u0026#39;, help=\u0026#39;AWS region to communicate with for API calls [default: us-east-1]\u0026#39;, ) parser.add_argument( \u0026#39;-f\u0026#39;, \u0026#39;--force\u0026#39;, env_var=\u0026#39;FORCE\u0026#39;, action=\u0026#39;store_true\u0026#39;, default=False, help=\u0026#39;force replacement of instances, even if AMI matches latest\u0026#39;, ) parser.add_argument( \u0026#39;-l\u0026#39;, \u0026#39;--log_level\u0026#39;, env_var=\u0026#39;LOG_LEVEL\u0026#39;, default=\u0026#39;INFO\u0026#39;, help=\u0026#39;log level of logger (DEBUG, INFO, WARNING, ERROR, CRITICAL) [default: INFO]\u0026#39;, ) self.args = parser.parse_args() def __init_log(self): if self.args.log_level.upper() in list(logging._nameToLevel.keys()): level = logging.getLevelName(self.args.log_level.upper()) else: level = logging.INFO logging.basicConfig( stream=sys.stdout, level=level, format=\u0026#39;%(asctime)s | %(levelname)s | %(message)s\u0026#39;, ) self.logger = logging.getLogger() def roll_instances(self, asg_name, cluster): for instance in self.ecs.cluster_instances: if instance[\u0026#39;instance_id\u0026#39;] not in self.asg.instances: continue ami_id = self.asg.instance_ami(instance[\u0026#39;instance_id\u0026#39;]) if (ami_id == self.asg.latest_ami) and (not self.args.force): self.logger.info(f\u0026#39;AMI for {instance} is up to date and FORCE not set, not rotating\u0026#39;) continue drain_time = self.detach_and_drain(instance) self.deregister(instance, asg_name) self.logger.info(f\u0026#34;terminating {instance[\u0026#39;instance_id\u0026#39;]}\u0026#34;) self.asg.terminate_instance(instance[\u0026#39;instance_id\u0026#39;]) if (asg_name == self.ecs.cluster_asgs[-1]) and (instance[\u0026#39;instance_id\u0026#39;] == self.asg.instances[-1]): return self.sleep(drain_time) def detach_and_drain(self, instance): self.logger.info(f\u0026#34;detaching {instance[\u0026#39;instance_id\u0026#39;]} from ASG\u0026#34;) self.asg.detach_instance_from_asg(instance[\u0026#39;instance_id\u0026#39;]) self.logger.info(f\u0026#34;draining {instance[\u0026#39;instance_id\u0026#39;]} of all container tasks....\u0026#34;) self.ecs.drain_instance(instance) drain_time = 0 while True: count = self.ecs.instance_task_count(instance) if count == 0: break time.sleep(2) drain_time += 2 if drain_time \u0026gt; 3600: raise TimeoutError(f\u0026#39;{instance} took too long to drain, maybe it is stuck?\u0026#39;) sys.exit(1) return drain_time def deregister(self, instance, asg_name): self.logger.info(f\u0026#34;deregistering {instance[\u0026#39;instance_id\u0026#39;]} from ECS cluster\u0026#34;) self.ecs.deregister_instance(instance) if (self.asg.orig_desired != self.asg.curr_capacity()) and (self.asg.curr_capacity() \u0026lt; self.asg.orig_desired): raise AssertionError(f\u0026#39;{asg_name} ASG doesnt have as many instances as it originally had, maybe it cant make new instances?\u0026#39;) sys.exit(1) def sleep(self, drain_time): base_time = 900 if self.asg.platform.lower().startswith(\u0026#39;windows\u0026#39;) else 120 sleep_time = base_time - (drain_time * 0.25) if drain_time \u0026lt; 120 else 0 if sleep_time != 0: self.logger.info(f\u0026#39;sleeping {sleep_time} seconds before moving to next server\u0026#39;) time.sleep(int(sleep_time)) def main(): updater = Updater() updater.ecs = ECS(updater.args.cluster, updater.boto_session, updater.boto_config) for asg_name in updater.ecs.cluster_asgs: updater.asg = ASG(asg_name, updater.boto_session, updater.boto_config) updater.logger.info(f\u0026#34;found AMI for {asg_name}: {updater.asg.latest_ami}\u0026#34;) if updater.asg.lt_curr_ami == updater.asg.latest_ami: updater.logger.info(f\u0026#39;ami for {asg_name} is up to date\u0026#39;) else: updater.logger.info(f\u0026#39;found newer AMI for {asg_name} ({updater.asg.latest_ami}), updating launch template\u0026#39;) updater.asg.update_launch_template() updater.asg.set_launch_template_version() updater.logger.info(f\u0026#39;rolling instances in the {updater.args.cluster} ECS cluster\u0026#39;) updater.roll_instances(asg_name, updater.args.cluster) updater.logger.info(\u0026#39;all instances have been replaced\u0026#39;) if __name__ == \u0026#34;__main__\u0026#34;: main() requirements.txt\nboto3==1.22.13 ConfigArgParse==1.5.3 Dockerfile\nFROM python:3.9-alpine COPY . / RUN pip install -r requirements.txt ENTRYPOINT [\u0026#34;/ecs_ami_updater\u0026#34;] README.md\n# ECS AMI Updater Safely replace instances in an Amazon ECS cluster with the latest AMI. ## Setup To install dependencies: ``` pip install -r requirements.txt ``` To build this for running in Docker, run: `docker build -t ecs_ami_updater .` ## Running To run this script locally, you will need to set the appropriate AWS environment variables that direct the SDK to load your desired configuration. Oftentimes, this is either the `AWS_ACCESS_KEY_ID` and secret or `AWS_PROFILE`. `AWS_REGION` will also likely need to be set unless already specified in your `~/.aws/config` file. For more information on how the AWS SDK uses environment variables, see this documentation. Here are the flags the command accepts. These arguments can be passed in as flags. ``` ❯ ./ecs_ami_updater -h usage: ecs_ami_updater [-h] -c CLUSTER [-f] [-r REGION] [-l LOG_LEVEL] options: -h, --help show this help message and exit -c CLUSTER, --cluster name of cluster for which to replace instances [env var: CLUSTER] -r REGION, --region REGION AWS region to communicate with for API calls [default: us-east-1] [env var: AWS_REGION] -f, --force force replacement of instances, even if AMI matches latest [env var: FORCE] -l LOG_LEVEL, --log_level LOG_LEVEL log level of logger (DEBUG, INFO, WARNING, ERROR, CRITICAL) [default: INFO] If an ARG is specified in more than one place, then command line values override environment variables, which override defaults. ``` To run this command line from Docker, you will need to pass the same environment variables outlined in the previous section into the docker via its `-e` flag. Additionally, you will need to expose your `.aws/` config directory to Docker (the `-v` flag), if using a profile. A full command line example would be: ``` ❯ AWS_PROFILE={profile} AWS_REGION=us-east-1 docker run --rm -e AWS_PROFILE -e AWS_REGION -v ~/.aws:/root/.aws ecs_ami_updater {ARGS} ``` ## Permissions This script needs the following permissions from AWS Identity and Access Management (AWS IAM), which provides fine-grained access control across all AWS. You can limit this script to the ARNs of the ASGs and Amazon ECS cluster. You can attach the policy below to a Task Role, which will be used by the automation script. For Amazon EC2, it’s recommended to break out `TerminateInstances` so that it can be defined with some condition, for example, only allow termination of Amazon ECS instances given a common tag. You can also refer to the policy description on ECS IAM policies. ```json { \u0026#34;Version\u0026#34;: \u0026#34;2012-10-17\u0026#34;, \u0026#34;Statement\u0026#34;: [ { \u0026#34;Sid\u0026#34;: \u0026#34;ASGAccess\u0026#34;, \u0026#34;Action\u0026#34;: [ \u0026#34;autoscaling:DescribeAutoScalingGroups\u0026#34;, \u0026#34;autoscaling:DetachInstances\u0026#34; ], \u0026#34;Effect\u0026#34;: \u0026#34;Allow\u0026#34;, \u0026#34;Resource\u0026#34;: \u0026#34;*\u0026#34; }, { \u0026#34;Sid\u0026#34;: \u0026#34;EC2Access\u0026#34;, \u0026#34;Action\u0026#34;: [ \u0026#34;ec2:CreateLaunchTemplateVersion\u0026#34;, \u0026#34;ec2:DescribeImages\u0026#34;, \u0026#34;ec2:DescribeInstances\u0026#34;, \u0026#34;ec2:DescribeLaunchTemplateVersions\u0026#34;, \u0026#34;ec2:ModifyLaunchTemplate\u0026#34;, \u0026#34;ec2:TerminateInstances\u0026#34; ], \u0026#34;Effect\u0026#34;: \u0026#34;Allow\u0026#34;, \u0026#34;Resource\u0026#34;: \u0026#34;*\u0026#34; }, { \u0026#34;Sid\u0026#34;: \u0026#34;ClusterDescribe\u0026#34;, \u0026#34;Action\u0026#34;: [ \u0026#34;ecs:DescribeClusters\u0026#34; ], \u0026#34;Effect\u0026#34;: \u0026#34;Allow\u0026#34;, \u0026#34;Resource\u0026#34;: \u0026#34;arn:aws:ecs:AWS_REGION:ACCOUNT_ID:cluster/CLUSTER_NAME\u0026#34; }, { \u0026#34;Sid\u0026#34;: \u0026#34;CapacityProviderDescribe\u0026#34;, \u0026#34;Action\u0026#34;: [ \u0026#34;ecs:DescribeCapacityProviders\u0026#34; ], \u0026#34;Effect\u0026#34;: \u0026#34;Allow\u0026#34;, \u0026#34;Resource\u0026#34;: \u0026#34;*\u0026#34; }, { \u0026#34;Sid\u0026#34;: \u0026#34;ECSAccess\u0026#34;, \u0026#34;Action\u0026#34;: [ \u0026#34;ecs:DeregisterContainerInstance\u0026#34;, \u0026#34;ecs:DescribeContainerInstances\u0026#34;, \u0026#34;ecs:ListContainerInstances\u0026#34;, \u0026#34;ecs:UpdateContainerInstancesState\u0026#34; ], \u0026#34;Effect\u0026#34;: \u0026#34;Allow\u0026#34;, \u0026#34;Resource\u0026#34;: \u0026#34;arn:aws:ecs:AWS_REGION:ACCOUNT_ID:container-instance/CLUSTER_NAME/*\u0026#34; } ] } ``` Put this automated process to work for your business The robust features built into Amazon ECS, Amazon EC2, and Amazon EC2 Auto Scaling groups help you to orchestrate patching to automate the process of replacing hosts. If you need help implementing this process or you want to discuss your specific automation needs, contact your AWS account team today.\n","permalink":"https://nckslvr.mn/posts/automate-patching-by-replacing-amazon-ecs-container-instances/","summary":"\u003cp\u003e\u003cem\u003eOriginally published on \u003ca href=\"https://aws.amazon.com/blogs/industries/automate-patching-by-replacing-amazon-ecs-container-instances/\"\u003eAWS for Industries\u003c/a\u003e.\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eBy Nick Silverman, Jigna Gandhi, Kim Fushimi, and Randy Moy.\u003c/em\u003e\u003c/p\u003e\n\u003ch2 id=\"containerization-becoming-popular-for-application-lifecycle\"\u003eContainerization becoming popular for application lifecycle\u003c/h2\u003e\n\u003cp\u003eIn the retail industry, more and more developers are using containerization as the primary software lifecycle for applications and services. There are countless benefits to this approach, and it shifts the focus of configuration and runtime management to the container itself. This means that there’s much less configuration and setup at the host level. However, you still need to consider patching and security at the host.\u003c/p\u003e","title":"Automate Patching by Replacing Amazon ECS Container Instances"},{"content":"Originally published on DEV Community.\nThe latest Ruby runtime for AWS Lambda runs Ruby 2.7. Though this version of ruby is only 6 months old, the version of OpenSSL that Lambdas instance of Ruby was compiled with is over 3 years old. You can verify that by running the function below and seeing what it returns:\nrequire \u0026#39;openssl\u0026#39; def lambda_handler(event:, context:) return OpenSSL::OPENSSL_VERSION end # OpenSSL 1.0.2k 26 Jan 2017 That\u0026rsquo;s Old! That means that Ruby\u0026rsquo;s OpenSSL library is missing some key features like SHA-3, TLS 1.3, and the scrypt KDF.\nI wanted to see if I could load in a newer version of the OpenSSL shared library ruby loads so I could leverage some of these shiny new features. Well, it turns out, AWS Lambda Layers was a big part of the answer here. In the documentation, a Lambda Layer is available to your Lambda code via the /opt directory. Now anyone who uses a lot of gem dependencies might have already come across this feature as it\u0026rsquo;s a great way to share gems across different functions while keeping the function size itself fairly small.\nBut interestingly enough, it\u0026rsquo;s not just a place to load gems. Lambda also adds to the RUBYLIB environment variable with a path you can fill with a Lambda Layer (specifically /opt/ruby/lib). This path is also prefixed to the LOAD_PATH variable. This is where things get interesting.\nNow that we know we can load up a Lambda Layer with a shared library that will be part of the auto searched LOAD_PATH, we can construct a Lambda Layer with the necessary files to load our own version of OpenSSL. To do this, we need a newer instance of openssl.so that was compiled with Ruby and we also need the libssl.so.1.1 and libcrypto.so.1.1 files to support the shared library.\nI was able to extract a copy of these files by installing the latest version of OpenSSL from my package manager (pacman), and installing Ruby 2.7 from RVM so it re-compiled on my machine. In the end, I constructed a directory structure that looked like this:\n. ├── lib │ ├── libcrypto.so -\u0026gt; libcrypto.so.1.1 │ ├── libcrypto.so.1.1 │ ├── libssl.so -\u0026gt; libssl.so.1.1 │ └── libssl.so.1.1 └── ruby └── lib └── openssl.so I then zipped that up and uploaded that zip to a new Lambda Layer destined for my function. Upon running the below function, we can see that my OpenSSL version is now nice and new and should include the features I want! Running the original function above, I now see OpenSSL 1.1.1d 10 Sep 2019. Excellent! Now I can go generate all the scrypt keys and initiate all the TLS 1.3 connections I want right?\nNot exactly. It turns out, Ruby has a fun little behavior when it sees it needs to load some files. when calling require, ruby will search through the LOAD_PATH for the code you are trying to load, but specifically with require, it will load .rb files and shared libraries with the .so extension. So When I tried to create a new SHA-256 digest, I was met with an unexpected error:\nrequire \u0026#39;openssl\u0026#39; def lambda_handler(event:, context:) return OpenSSL::Digest::SHA256.new end # uninitialized constant OpenSSL::Digest::SHA256 What happened? Well it turns out, because my openssl.so file is now ahead of Ruby\u0026rsquo;s built-in openssl.rb code, I am only loading the shared library which comes with some classes, but not all the classes I expect. To get around this, it\u0026rsquo;s quite simple:\nrequire \u0026#39;openssl.rb\u0026#39; def lambda_handler(event:, context:) return OpenSSL::Digest::SHA256.new end # #\u0026lt;OpenSSL::Digest::SHA256: ...\u0026gt; By specifying the .rb extension, I am now instructing Ruby to look through its LOAD_PATH until it finds the first instance of a file called openssl.rb. This is included with ruby and is the code that loads in all of the classes I expect to see, as well as an explicit call to load openssl.so. This now allows me to use all of the shiny new features that OpenSSL 1.1.1(x) provides without having to use a Custom Runtime.\n","permalink":"https://nckslvr.mn/posts/overwriting-shared-libraries-in-aws-lambda/","summary":"\u003cp\u003e\u003cem\u003eOriginally published on \u003ca href=\"https://dev.to/nckslvrmn/overwriting-shared-libraries-in-aws-lambda-479h\"\u003eDEV Community\u003c/a\u003e.\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eThe latest Ruby runtime for AWS Lambda runs Ruby 2.7. Though this version of ruby is only 6 months old, the version of OpenSSL that Lambdas instance of Ruby was compiled with is over 3 years old. You can verify that by running the function below and seeing what it returns:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-ruby\" data-lang=\"ruby\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003erequire\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;openssl\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003elambda_handler\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"ss\"\u003eevent\u003c/span\u003e\u003cspan class=\"p\"\u003e:,\u003c/span\u003e \u003cspan class=\"ss\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e:)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"no\"\u003eOpenSSL\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"no\"\u003eOPENSSL_VERSION\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eend\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# OpenSSL 1.0.2k  26 Jan 2017\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThat\u0026rsquo;s Old! That means that Ruby\u0026rsquo;s OpenSSL library is missing some key features like \u003ccode\u003eSHA-3\u003c/code\u003e, \u003ccode\u003eTLS 1.3\u003c/code\u003e, and the \u003ccode\u003escrypt\u003c/code\u003e KDF.\u003c/p\u003e","title":"Overwriting Shared Libraries in Aws Lambda"}]