Skip to content

Instantly share code, notes, and snippets.

@jhw
Last active March 30, 2024 18:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jhw/fe1d085c37b6f7246b9e5f4fcbf850f4 to your computer and use it in GitHub Desktop.
Save jhw/fe1d085c37b6f7246b9e5f4fcbf850f4 to your computer and use it in GitHub Desktop.
Can you replicate an APIGWV1 website pattern with APIGWV2?
env
*.pyc
__pycache__
tmp
setenv-priv.sh
AppName=apigwv2-website-demo
#!/usr/bin/env bash
. app.props
aws cloudformation delete-stack --stack-name $AppName
#!/usr/bin/env bash
. app.props
echo "DomainName: $DOMAIN_NAME"
echo "CertificateArn: $CERTIFICATE_ARN"
aws cloudformation deploy --stack-name $AppName --template-file stack.json --capabilities CAPABILITY_NAMED_IAM --parameter-overrides DomainName=$DOMAIN_NAME CertificateArn=$CERTIFICATE_ARN
from botocore.exceptions import ClientError
import boto3, os, re, sys, time
def fetch_log_events(logs, kwargs):
events, token = [], None
while True:
if token:
kwargs["nextToken"]=token
resp=logs.filter_log_events(**kwargs)
events+=resp["events"]
if "nextToken" in resp:
token=resp["nextToken"]
else:
break
return sorted(events,
key=lambda x: x["timestamp"])
if __name__=="__main__":
try:
if len(sys.argv) < 3:
raise RuntimeError("please enter lambda name, window")
lambdaname, window = sys.argv[1:3]
if not re.search("^\\d+$", window):
raise RuntimeError("window is invalid")
window=int(window)
logs=boto3.client("logs")
starttime=int(1000*(time.time()-window))
loggroupname="/aws/lambda/%s" % lambdaname
kwargs={"logGroupName": loggroupname,
"startTime": starttime,
"interleaved": True}
events=fetch_log_events(logs, kwargs)
for event in events:
msg=re.sub("\\r|\\n", "", event["message"])
print (msg)
except RuntimeError as error:
print ("Error: %s" % str(error))
except ClientError as error:
print ("Error: %s" % str(error))
from botocore.exceptions import ClientError
import boto3, os, re, sys
def hungarorise(text):
return "".join([tok.capitalize()
for tok in re.split("\\-|\\_", text)])
def fetch_outputs(cf, stackname):
outputs={}
for stack in cf.describe_stacks()["Stacks"]:
if (stack["StackName"].startswith(stackname) and
"Outputs" in stack):
for output in stack["Outputs"]:
outputs[output["OutputKey"]]=output["OutputValue"]
return outputs
if __name__=="__main__":
try:
props=dict([tuple(row.split("="))
for row in open("app.props").read().split("\n")
if row!=''])
stackname=props["AppName"]
cf=boto3.client("cloudformation")
outputs=fetch_outputs(cf, stackname)
bucketkey=hungarorise("app-bucket")
if bucketkey not in outputs:
raise RuntimeError("bucket not found")
bucketname=outputs[bucketkey]
s3=boto3.client("s3")
paginator=s3.get_paginator("list_objects_v2")
pages=paginator.paginate(Bucket=bucketname)
for struct in pages:
if "Contents" in struct:
for obj in struct["Contents"]:
print (obj["Key"])
s3.delete_object(Bucket=bucketname,
Key=obj["Key"])
except RuntimeError as error:
print ("Error: %s" % str(error))
except ClientError as error:
print ("Error: %s" % str(error))
import json, os
if __name__=="__main__":
if not os.path.exists("tmp"):
os.mkdir("tmp")
struct=json.loads(open("stack.json").read())
with open("tmp/stack.json", 'w') as f:
f.write(json.dumps(struct,
indent=2))
<html>
<head>
<title>Hello World</title>
</head>
<body>
<div>Hello World!</div>
</body>
</html>
#!/usr/bin/env bash
. app.props
aws cloudformation describe-stack-events --stack-name $AppName --query "StackEvents[].{\"1.Timestamp\":Timestamp,\"2.Id\":LogicalResourceId,\"3.Type\":ResourceType,\"4.Status\":ResourceStatus,\"5.Reason\":ResourceStatusReason}"
#!/usr/bin/env bash
. app.props
aws cloudformation describe-stacks --stack-name $AppName --query 'Stacks[0].Outputs' --output table
#!/usr/bin/env bash
. app.props
aws cloudformation describe-stack-resources --stack-name $AppName --query "StackResources[].{\"1.Timestamp\":Timestamp,\"2.LogicalId\":LogicalResourceId,\"3.PhysicalId\":PhysicalResourceId,\"4.Type\":ResourceType,\"5.Status\":ResourceStatus}"
#!/usr/bin/env bash
aws cloudformation describe-stacks --query "Stacks[].{\"1.Name\":StackName,\"2.Status\":StackStatus}"
from botocore.exceptions import ClientError
import boto3, os, re, sys
def hungarorise(text):
return "".join([tok.capitalize()
for tok in re.split("\\-|\\_", text)])
def fetch_outputs(cf, stackname):
outputs={}
for stack in cf.describe_stacks()["Stacks"]:
if (stack["StackName"].startswith(stackname) and
"Outputs" in stack):
for output in stack["Outputs"]:
outputs[output["OutputKey"]]=output["OutputValue"]
return outputs
if __name__=="__main__":
try:
props=dict([tuple(row.split("="))
for row in open("app.props").read().split("\n")
if row!=''])
stackname=props["AppName"]
cf=boto3.client("cloudformation")
outputs=fetch_outputs(cf, stackname)
bucketkey=hungarorise("app-bucket")
if bucketkey not in outputs:
raise RuntimeError("bucket not found")
bucketname=outputs[bucketkey]
s3=boto3.client("s3")
print (s3.put_object(Bucket=bucketname,
Key="index.html",
Body=open("index.html").read(),
ContentType="text/html"))
except RuntimeError as error:
print ("Error: %s" % str(error))
except ClientError as error:
print ("Error: %s" % str(error))
awscli
boto3
botocore
pyyaml
requests
#!/usr/bin/env bash
export AWS_DEFAULT_OUTPUT=table
export AWS_PROFILE=#{your-aws-profile-here}
export CERTIFICATE_ARN=#{your-certificate-arn-here}
export DOMAIN_NAME=#{your-fully-qualified-domain-name-here}
{
"Outputs": {
"AppBucket": {
"Value": {
"Ref": "AppBucket"
}
},
"AppRestApi": {
"Value": {
"Ref": "AppRestApi"
}
}
},
"Parameters": {
"CertificateArn": {
"Type": "String"
},
"DomainName": {
"Type": "String"
}
},
"Resources": {
"AppBasePathMapping": {
"Properties": {
"DomainName": {
"Ref": "DomainName"
},
"RestApiId": {
"Ref": "AppRestApi"
},
"Stage": {
"Ref": "AppStage"
}
},
"Type": "AWS::ApiGateway::BasePathMapping"
},
"AppBucket": {
"Properties": {
"NotificationConfiguration": {
"EventBridgeConfiguration": {
"EventBridgeEnabled": true
}
}
},
"Type": "AWS::S3::Bucket"
},
"AppDeployment": {
"DependsOn": [
"AppProxyMethod",
"AppRedirectMethod"
],
"Properties": {
"RestApiId": {
"Ref": "AppRestApi"
}
},
"Type": "AWS::ApiGateway::Deployment"
},
"AppDomainName": {
"Properties": {
"CertificateArn": {
"Ref": "CertificateArn"
},
"DomainName": {
"Ref": "DomainName"
}
},
"Type": "AWS::ApiGateway::DomainName"
},
"AppPolicy": {
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"s3:GetObject"
],
"Effect": "Allow",
"Resource": {
"Fn::Sub": "arn:aws:s3:::${AppBucket}/*"
}
}
],
"Version": "2012-10-17"
},
"PolicyName": {
"Fn::Sub": "app-policy-${AWS::StackName}"
},
"Roles": [
{
"Ref": "AppRole"
}
]
},
"Type": "AWS::IAM::Policy"
},
"AppProxyMethod": {
"Properties": {
"AuthorizationType": "NONE",
"HttpMethod": "GET",
"Integration": {
"Credentials": {
"Fn::GetAtt": [
"AppRole",
"Arn"
]
},
"IntegrationHttpMethod": "ANY",
"IntegrationResponses": [
{
"ResponseParameters": {
"method.response.header.Content-Type": "integration.response.header.Content-Type"
},
"StatusCode": 200
},
{
"SelectionPattern": "404",
"StatusCode": 404
}
],
"PassthroughBehavior": "WHEN_NO_MATCH",
"RequestParameters": {
"integration.request.path.proxy": "method.request.path.proxy"
},
"Type": "AWS",
"Uri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:s3:path/${AppBucket}/{proxy}"
}
},
"MethodResponses": [
{
"ResponseParameters": {
"method.response.header.Content-Type": true
},
"StatusCode": 200
},
{
"StatusCode": 404
}
],
"RequestParameters": {
"method.request.path.proxy": true
},
"ResourceId": {
"Ref": "AppResource"
},
"RestApiId": {
"Ref": "AppRestApi"
}
},
"Type": "AWS::ApiGateway::Method"
},
"AppRecordSet": {
"Properties": {
"AliasTarget": {
"DNSName": {
"Fn::GetAtt": [
"AppDomainName",
"DistributionDomainName"
]
},
"EvaluateTargetHealth": false,
"HostedZoneId": {
"Fn::GetAtt": [
"AppDomainName",
"DistributionHostedZoneId"
]
}
},
"HostedZoneName": {
"Fn::Sub": [
"${prefix}.${suffix}.",
{
"prefix": {
"Fn::Select": [
1,
{
"Fn::Split": [
".",
{
"Ref": "DomainName"
}
]
}
]
},
"suffix": {
"Fn::Select": [
2,
{
"Fn::Split": [
".",
{
"Ref": "DomainName"
}
]
}
]
}
}
]
},
"Name": {
"Ref": "DomainName"
},
"Type": "A"
},
"Type": "AWS::Route53::RecordSet"
},
"AppRedirectMethod": {
"Properties": {
"AuthorizationType": "NONE",
"HttpMethod": "GET",
"Integration": {
"IntegrationResponses": [
{
"ResponseParameters": {
"method.response.header.Location": {
"Fn::Sub": "'https://${DomainName}/index.html'"
}
},
"ResponseTemplates": {
"application/json": "{}"
},
"StatusCode": 302
}
],
"RequestTemplates": {
"application/json": "{\"statusCode\" : 302}"
},
"Type": "MOCK"
},
"MethodResponses": [
{
"ResponseParameters": {
"method.response.header.Location": true
},
"StatusCode": 302
}
],
"ResourceId": {
"Fn::GetAtt": [
"AppRestApi",
"RootResourceId"
]
},
"RestApiId": {
"Ref": "AppRestApi"
}
},
"Type": "AWS::ApiGateway::Method"
},
"AppResource": {
"Properties": {
"ParentId": {
"Fn::GetAtt": [
"AppRestApi",
"RootResourceId"
]
},
"PathPart": "{proxy+}",
"RestApiId": {
"Ref": "AppRestApi"
}
},
"Type": "AWS::ApiGateway::Resource"
},
"AppRestApi": {
"Properties": {
# "BinaryMediaTypes": ["*/*"],
"Name": {
"Fn::Sub": "app-rest-api-${AWS::StackName}"
}
},
"Type": "AWS::ApiGateway::RestApi"
},
"AppRole": {
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": [
"sts:AssumeRole"
],
"Effect": "Allow",
"Principal": {
"Service": "apigateway.amazonaws.com"
}
}
],
"Version": "2012-10-17"
}
},
"Type": "AWS::IAM::Role"
},
"AppStage": {
"Properties": {
"DeploymentId": {
"Ref": "AppDeployment"
},
"RestApiId": {
"Ref": "AppRestApi"
},
"StageName": "prod"
},
"Type": "AWS::ApiGateway::Stage"
}
}
}

short

  • test redirect vs binary media types

done

  • revert v2
  • "Regional" doesn't work; replace with "Distribution"
  • why does certificate need to be in us-east-1?
  • BinaryMediaTypes needs to be an array, if included
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment