diff --git a/main.go b/main.go new file mode 100644 index 0000000..544c662 --- /dev/null +++ b/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "os" + "text/template" +) + +type TmplData struct { + ApiProtocol string + ApiEndpoints string + LambdaFunctionName string + ApiProjectName string +} + +var allowedAPIProtocols = []string{"rest", "websocket"} +var allowedRestAPIEndpoints = []string{"regional", "edge", "private"} + +func main() { + apiTmpl := TmplData{ + ApiProtocol: "rest", + ApiEndpoints: "regional", + LambdaFunctionName: "helloworld", + ApiProjectName: "Hello-World-API", + } + + t := template.Must(template.New("apigw").Parse(apiGWConf)) + err := t.Execute(os.Stdout, apiTmpl) + if err != nil { + panic(err) + } +} diff --git a/tmpl-readme.go b/tmpl-readme.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/tmpl-readme.go @@ -0,0 +1 @@ +package main diff --git a/tmpl-sam.go b/tmpl-sam.go new file mode 100644 index 0000000..f961411 --- /dev/null +++ b/tmpl-sam.go @@ -0,0 +1,172 @@ +package main + +const apiGWConf = ` +--- +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 + +{{ if and (eq .ApiEndpoints "private") }} +Parameters: + VPCId: + Description: ID of the VPC ID + Type: AWS::EC2::VPC::Id + SubnetIDs: + Description: A list/array of Subnet IDs + Type: List + Environment: + Description: name of the environment + Type: String + AllowedValues: [test, prod] +{{ else }} +Parameters: + Environment: + Description: name of the environment + Type: String + AllowedValues: [test, prod] +{{ end}} + +Conditions: + IsProd: + !Equals [!Ref Environment, "prod"] + +Resources: + +{{ if and (eq .ApiEndpoints "private") }} + ######################## + # Infra stuff + ######################## + + LambdaSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: SG for private lambfa functions + GroupName: vpc-lambda + VpcId: !Ref VPCId + SecurityGroupIngress: + - IpProtocol: tcp + CidrIp: 172.31.190.0/24 + FromPort: 0 + ToPort: 65535 + - IpProtocol: tcp + CidrIp: 172.31.178.0/23 + FromPort: 0 + ToPort: 65535 + - IpProtocol: tcp + CidrIp: 10.168.58.0/24 + FromPort: 0 + ToPort: 65535 + - IpProtocol: tcp + CidrIp: 172.31.176.0/23 + FromPort: 0 + ToPort: 65535 + + ApiGwVpcEndpoint: + Type: AWS::EC2::VPCEndpoint + Properties: + ServiceName: !Sub "com.amazonaws.${AWS::Region}.execute-api" + PrivateDnsEnabled: true + VpcEndpointType: Interface + VpcId: !Ref VPCId + SubnetIds: !Ref SubnetIDs +{{ end }} + + ######################## + # API GW Conf + ######################## + + AnalyticsApi: + Type: AWS::Serverless::Api + Properties: + StageName: !Ref Environment + TracingEnabled: true # Enable X-Ray for distributed tracing to help debugging + {{ if and (eq .ApiEndpoints "private")}}EndpointConfiguration: PRIVATE{{ end }}{{ if and (eq .ApiEndpoints "regional")}}EndpointConfiguration: REGIONAL{{ end }}{{ if and (eq .ApiEndpoints "edge")}}EndpointConfiguration: EDGE{{ end }} + # Use DefinitionBody for swagger file so that we can use CloudFormation functions within the swagger file + DefinitionBody: + 'Fn::Transform': + Name: 'AWS::Include' + Parameters: + Location: ./swagger-api.yml + MethodSettings: + - ResourcePath: '/*' + HttpMethod: '*' + LoggingLevel: INFO + MetricsEnabled: true # Enable detailed metrics + DataTraceEnabled: true # Put logs into cloudwatch + AccessLogSetting: + DestinationArn: !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:${ApiAccessLogGroup}" + Format: '$context.identity.sourceIp $context.authorizer.claims.sub [$context.requestTime] "$context.httpMethod $context.resourcePath $context.protocol" $context.status $context.requestId $context.awsEndpointRequestId $context.xrayTraceId $context.responseLatency $context.integrationLatency "$context.error.message"' + Cors: + AllowOrigin: "'*'" + AllowHeaders: "'content-type'" + + ######################## + # IAM for API GW + ######################## + + # This role allows API Gateway to push execution and access logs to CloudWatch logs + ApiGatewayPushToCloudWatchRole: + Type: "AWS::IAM::Role" + Properties: + Description: "Push logs to CloudWatch logs from API Gateway" + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Service: apigateway.amazonaws.com + Action: "sts:AssumeRole" + ManagedPolicyArns: + - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" + + ApiAccount: + Type: "AWS::ApiGateway::Account" + Properties: + CloudWatchRoleArn: !GetAtt ApiGatewayPushToCloudWatchRole.Arn + + ApiAccessLogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub "/aws/apigateway/AccessLog-${AnalyticsApi}" + RetentionInDays: 365 + + + ######################## + # Functions Goes Here + ######################## + + RecommendationsFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: src/recommendations/app/ + Handler: recommendations.lambda_handler + Runtime: python3.7 + MemorySize: 512 + Timeout: 5 + Tracing: Active + Policies: + - AWSLambdaExecute + Layers: + - !Ref RecommendationsLayer + Events: + AnyApi: + Type: Api + Properties: + RestApiId: !Ref AnalyticsApi + Path: '/recommendations/{userId}' + Method: GET + + RecommendationsLayer: + Type: AWS::Serverless::LayerVersion + Properties: + LayerName: recommendations-deps + Description: Dependencies for RecommendationsFunction + ContentUri: src/recommendations/dependencies/ + CompatibleRuntimes: + - python3.7 + RetentionPolicy: Retain + +Outputs: + ApiURL: + Description: {{ .ApiProjectName }} + Value: !Sub 'https://${AnalyticsApi}.execute-api.${AWS::Region}.amazonaws.com/${Environment}/' +` diff --git a/tmpl-swagger.go b/tmpl-swagger.go new file mode 100644 index 0000000..176256a --- /dev/null +++ b/tmpl-swagger.go @@ -0,0 +1,79 @@ +package main + +const swagger = ` +--- +openapi: "3.0.1" +{{ if and (eq .apiEndpoints "private") }} +x-amazon-apigateway-policy: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: "*" + Action: + - "execute-api:Invoke" + Resource: "execute-api:/*" + Condition: + StringEquals: + aws:SourceVpc: + Ref: VPCId +{{ else }} +{{ end }} + +info: + title: {{ .apiProjectName }} + description: your awesome description here + version: "v1.0" + +servers: + - url: https://apigw-url.example.com/prod + description: Test environment URL + - url: http://apigw-url.example.com/test + description: Production environment URL + +paths: + /v1/{{ .lambdaFunctionName }}/{userId}: + get: + summary: hello world endpoint + description: outputs hello world + parameters: + - in: path + name: userId + schema: + type: integer + required: true + responses: + 200: + description: "OK" + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/ArticleObj" + 500: + description: "Internal Server Error" + content: {} + x-amazon-apigateway-integration: + uri: + Fn::Sub: "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloWorldFunction.Arn}/invocations" + httpMethod: POST + passthroughBehavior: "when_no_match" + type: aws_proxy +components: + schemas: + ArticleObj: + properties: + rowValues: + type: "array" + items: + type: "object" + properties: + modelId: + type: "number" + Article_Id: + type: "string" + MU_UOM_Cd: + type: "string" + rank: + type: "number" +`