Hosting websites from private S3 buckets

At work, we were alerted to an outage of an S3-backed frontend. The frontend was returning 403 responses. This left us scratching our head, as no deployment had occurred recently. After doing some digging, we found that AWS account administrators had applied a new policy to make all S3 buckets private (this is an account-wide setting, overriding bucket-level settings). 🆒 🆒 🆒 So how can we configure Cloudfront to access private S3 buckets? After a bit of experimentation, here are my findings: ...

April 19, 2024 · 4 min

An ECS -> RDS Security Group Script

Below is a simple script to allow a user to alter RDS databases security groups to allow access from an ECS Service. Useful when we have an observability tool runing in ECS that wants to add RDS data connections. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 from typing import List, Dict import boto3 from botocore.exceptions import ClientError import inquirer def list_ecs_clusters(): ecs = boto3.client("ecs") clusters = ecs.list_clusters() cluster_arns = clusters["clusterArns"] return cluster_arns def list_ecs_services(cluster) -> List[str]: ecs = boto3.client("ecs") services = ecs.list_services(cluster=cluster) return services["serviceArns"] def list_rds_instances() -> Dict[str, str]: rds = boto3.client("rds") return rds.describe_db_instances()["DBInstances"] def get_security_group_from_ecs_service(cluster, service_arn) -> str: ecs = boto3.client("ecs") details = ecs.describe_services(cluster=cluster, services=[service_arn]) service = details["services"][0] deployment = service["deployments"][0] groups = deployment["networkConfiguration"]["awsvpcConfiguration"]["securityGroups"] return groups[0] def get_security_group_ids_for_rds_instance(instance_identifier) -> List[str]: """ Returns the security group IDs for a given RDS instance identifier. :param instance_identifier: The identifier of the RDS instance :return: A list of security group IDs associated with the RDS instance """ rds = boto3.client("rds") try: response = rds.describe_db_instances(DBInstanceIdentifier=instance_identifier) db_instances = response["DBInstances"] if db_instances: # Assuming each instance has at least one security group associated with it security_groups = db_instances[0]["VpcSecurityGroups"] security_group_ids = [sg["VpcSecurityGroupId"] for sg in security_groups] return security_group_ids else: return [] except Exception as e: print( f"Error fetching security group IDs for RDS instance '{instance_identifier}': {e}" ) return [] def modify_security_group_rules( security_group_id, protocol, from_port, to_port, source_security_group_id: str, description: str, dry_run: bool, ) -> None: ec2 = boto3.client("ec2") if dry_run: print( f"Dry run: Would update Security Group {security_group_id} " f"to allow from {source_security_group_id}" ) return try: ec2.authorize_security_group_ingress( GroupId=security_group_id, IpPermissions=[ { "IpProtocol": protocol, "FromPort": from_port, "ToPort": to_port, "UserIdGroupPairs": [ { "GroupId": source_security_group_id, "Description": description, } ], } ], ) print(f"Security Group {security_group_id} updated successfully.") except ClientError as e: print(f"Error updating Security Group: {e}") if __name__ == "__main__": dry_run = inquirer.confirm( message="Dry run (no changes will be made)?", default=True, ) ecs_clusters = list_ecs_clusters() rds_instances = list_rds_instances() # Select ECS Cluster resource_questions = [ inquirer.List( "cluster", message="Select ECS Cluster that contains the service that requires databases access", choices=ecs_clusters, default=lambda answers: next( (cluster for cluster in ecs_clusters if "grafana" in cluster), None ), ), inquirer.List( "service", message="Select ECS Service in {cluster} that requires database access", choices=lambda answers: list_ecs_services(answers["cluster"]), ), inquirer.Checkbox( "rds_instances", message="Select RDS Instances that will be accessed via {cluster}/{service}", choices=[instance["DBInstanceIdentifier"] for instance in rds_instances], ), inquirer.Text( "description", message="Provide a description for the connection", default=lambda answers: f"Allow connections from ECS service: {answers['service'].split(':')[-1]}", ), ] resources = inquirer.prompt(resource_questions) # Get Security Group of selected RDS Instances rds_security_groups = [ get_security_group_ids_for_rds_instance(rds_instance)[0] for rds_instance in resources["rds_instances"] ] # Get Security Group of selected ECS Service ecs_security_group = get_security_group_from_ecs_service( resources["cluster"], resources["service"] ) # Update RDS Instances' Security Groups to allow inbound connections from ECS Service print(f"{rds_security_groups=}") for rds_sg_id in rds_security_groups: modify_security_group_rules( security_group_id=rds_sg_id, source_security_group_id=ecs_security_group, protocol="tcp", to_port=5432, from_port=5432, description=resources["description"], dry_run=dry_run, )

February 8, 2024 · 3 min

Normalizing heterogeneous decimal Ion data in Athena

Recently, we exported data from a DynamoDB table to S3 in AWS Ion format. However, due to the fact that the DynamoDB table had varied formats for some numeric properties, the export serialized these numeric data columns in a few different formats: as a decimal (1234.), as an Ion decimal type (1234d0), and as a string ("1234"). However, we want to be able to treat these values as a bigint within our Athena queries. ...

August 21, 2023 · 2 min

Security-conscious cloud deployments from Github Actions via OpenID Connect

Goals This ticket is focused on how we can securely deploy to a major cloud provider environment (e.g. AWS, Azure, GCP) from within our Github Actions workflows. Why is this challenging? A naive solution to this problem is to generate some cloud provider credentials (e.g. AWS Access Keys) and to store them as a Github Secret. Our Github Actions can then utilize these credentials in its workflows. However, this technique contains a number of concerns: ...

December 20, 2021 · 7 min

An ECR Deployment Script

Below is a simple script to deploy a Docker image to ECR… 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 set -e log () { local bold=$(tput bold) local normal=$(tput sgr0) echo "${bold}${1}${normal}" 1>&2; } if [ -z "${AWS_ACCOUNT}" ]; then log "Missing a valid AWS_ACCOUNT env variable"; exit 1; else log "Using AWS_ACCOUNT '${AWS_ACCOUNT}'"; fi AWS_REGION=${AWS_REGION:-us-east-1} REPO_NAME=${REPO_NAME:-my/repo} log "🔑 Authenticating..." aws ecr get-login-password \ --region ${AWS_REGION} \ | docker login \ --username AWS \ --password-stdin \ ${AWS_ACCOUNT}.dkr.ecr.${AWS_REGION}.amazonaws.com log "📦 Building image..." docker build -t ${REPO_NAME} . log "🏷️ Tagging image..." docker tag \ ${REPO_NAME}:latest \ ${AWS_ACCOUNT}.dkr.ecr.${AWS_REGION}.amazonaws.com/${REPO_NAME}:latest log "🚀 Pushing to ECR repo..." docker push \ ${AWS_ACCOUNT}.dkr.ecr.${AWS_REGION}.amazonaws.com/${REPO_NAME}:latest log "💃 Deployment Successful. 🕺"

November 9, 2020 · 1 min

Using CloudFront as a Reverse Proxy

Alternate title: How to be master of your domain. The basic idea of this post is to demonstrate how CloudFront can be utilized as a serverless reverse-proxy, allowing you to host all of your application’s content and services from a single domain. This minimizes a project’s TLD footprint while providing project organization and performance along the way. Why Within large organizations, bureaucracy can make it a challenge to obtain a subdomain for a project. This means that utilizing multiple service-specific subdomains (e.g. api.my-project.big-institution.gov or thumbnails.my-project.big-institution.gov) is an arduous process. To avoid this in a recent project, we settled on adopting a pattern where we use CloudFront to proxy all of our domain’s incoming requests to their appropriate service. ...

October 2, 2020 · 10 min

How to generate a database URI from an AWS Secret

A quick note about how to generate a database URI (or any other derived string) from an AWS SecretsManager SecretTargetAttachment (such as what’s provided via a RDS DatabaseInstance’s secret property). 1 2 3 4 5 6 7 8 9 10 11 db = rds.DatabaseInstance( # ... ) db_val = lambda field: db.secret.secret_value_from_json(field).to_string() task_definition.add_container( environment=dict( # ... PGRST_DB_URI=f"postgres://{db_val('username')}:{db_val('password')}@{db_val('host')}:{db_val('port')}/", ), # ... )

June 15, 2020 · 1 min

Tips for working with a large number of files in S3

I would argue that S3 is basically AWS’ best service. It’s super cheap, it’s basically infinitely scalable, and it never goes down (except for when it does). Part of its beauty is its simplicity. You give it a file and a key to identify that file, you can have faith that it will store it without issue. You give it a key, you can have faith that it will return the file represented by that key, assuming there is one. ...

May 30, 2020 · 7 min

Boilerplate for S3 Batch Operation Lambda

S3 Batch Operation provide a simple way to process a number of files stored in an S3 bucket with a Lambda function. However, the Lambda function must return particular Response Codes. Below is an example of a Lambda function written in Python that works with AWS S3 Batch Operations.

December 20, 2019 · 1 min

Using CloudFormation's Fn::Sub with Bash parameter substitution

Let’s say that you need to inject a large bash script into a CloudFormation AWS::EC2::Instance Resource’s UserData property. CloudFormation makes this easy with the Fn::Base64 intrinsic function: 1 2 3 4 5 6 7 8 9 10 11 12 AWSTemplateFormatVersion: '2010-09-09' Resources: VPNServerInstance: Type: AWS::EC2::Instance Properties: ImageId: ami-efd0428f InstanceType: m3.medium UserData: Fn::Base64: | #!/bin/sh echo "Hello world" In your bash script, you may even want to reference a parameter created elsewhere in the CloudFormation template. This is no problem with Cloudformation’s Fn::Sub instrinsic function: ...

April 30, 2018 · 3 min