diff --git a/aws-test/tests/aws_app_runner_service/dependencies.txt b/aws-test/tests/aws_app_runner_service/dependencies.txt new file mode 100644 index 000000000..e69de29bb diff --git a/aws-test/tests/aws_app_runner_service/test-get-expected.json b/aws-test/tests/aws_app_runner_service/test-get-expected.json new file mode 100644 index 000000000..779d2899b --- /dev/null +++ b/aws-test/tests/aws_app_runner_service/test-get-expected.json @@ -0,0 +1,7 @@ +[ + { + "arn": "{{ output.resource_aka.value}}", + "service_id": "{{ output.resource_id.value}}", + "service_name": "{{ resourceName }}" + } +] diff --git a/aws-test/tests/aws_app_runner_service/test-get-query.sql b/aws-test/tests/aws_app_runner_service/test-get-query.sql new file mode 100644 index 000000000..bae10b669 --- /dev/null +++ b/aws-test/tests/aws_app_runner_service/test-get-query.sql @@ -0,0 +1,3 @@ +select arn, service_name, service_id +from aws_app_runner_service +where arn = '{{ output.resource_aka.value }}' diff --git a/aws-test/tests/aws_app_runner_service/test-list-expected.json b/aws-test/tests/aws_app_runner_service/test-list-expected.json new file mode 100644 index 000000000..779d2899b --- /dev/null +++ b/aws-test/tests/aws_app_runner_service/test-list-expected.json @@ -0,0 +1,7 @@ +[ + { + "arn": "{{ output.resource_aka.value}}", + "service_id": "{{ output.resource_id.value}}", + "service_name": "{{ resourceName }}" + } +] diff --git a/aws-test/tests/aws_app_runner_service/test-list-query.sql b/aws-test/tests/aws_app_runner_service/test-list-query.sql new file mode 100644 index 000000000..2a1853a42 --- /dev/null +++ b/aws-test/tests/aws_app_runner_service/test-list-query.sql @@ -0,0 +1,3 @@ +select arn, service_name, service_id +from aws_app_runner_service +where akas::text = '["{{ output.resource_aka.value }}"]' diff --git a/aws-test/tests/aws_app_runner_service/test-turbot-expected.json b/aws-test/tests/aws_app_runner_service/test-turbot-expected.json new file mode 100644 index 000000000..93af667b0 --- /dev/null +++ b/aws-test/tests/aws_app_runner_service/test-turbot-expected.json @@ -0,0 +1,10 @@ +[ + { + "account_id": "{{output.account_id.value}}", + "akas": [ + "{{ output.resource_aka.value }}" + ], + "region": "{{output.aws_region.value}}", + "title": "{{ resourceName }}" + } +] diff --git a/aws-test/tests/aws_app_runner_service/test-turbot-query.sql b/aws-test/tests/aws_app_runner_service/test-turbot-query.sql new file mode 100644 index 000000000..cb19c8b87 --- /dev/null +++ b/aws-test/tests/aws_app_runner_service/test-turbot-query.sql @@ -0,0 +1,3 @@ +select title, akas, region, account_id +from aws_app_runner_service +where arn = '{{ output.resource_aka.value }}' diff --git a/aws-test/tests/aws_app_runner_service/variables.json b/aws-test/tests/aws_app_runner_service/variables.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/aws-test/tests/aws_app_runner_service/variables.json @@ -0,0 +1 @@ +{} diff --git a/aws-test/tests/aws_app_runner_service/variables.tf b/aws-test/tests/aws_app_runner_service/variables.tf new file mode 100644 index 000000000..49b21e5be --- /dev/null +++ b/aws-test/tests/aws_app_runner_service/variables.tf @@ -0,0 +1,87 @@ + +variable "resource_name" { + type = string + default = "turbot-test-20200125-create-update" + description = "Name of the resource used throughout the test." +} + +variable "aws_profile" { + type = string + default = "default" + description = "AWS credentials profile used for the test. Default is to use the default profile." +} + +variable "aws_region" { + type = string + default = "us-east-1" + description = "AWS region used for the test. Does not work with default region in config, so must be defined here." +} + +variable "aws_region_alternate" { + type = string + default = "us-east-2" + description = "Alternate AWS region used for tests that require two regions (e.g. DynamoDB global tables)." +} + +provider "aws" { + profile = var.aws_profile + region = var.aws_region +} + +provider "aws" { + alias = "alternate" + profile = var.aws_profile + region = var.aws_region_alternate +} + +data "aws_partition" "current" {} +data "aws_caller_identity" "current" {} +data "aws_region" "primary" {} +data "aws_region" "alternate" { + provider = aws.alternate +} + +data "null_data_source" "resource" { + inputs = { + scope = "arn:${data.aws_partition.current.partition}:::${data.aws_caller_identity.current.account_id}" + } +} + +resource "aws_apprunner_service" "named_test_resource" { + service_name = var.resource_name + + source_configuration { + image_repository { + image_configuration { + port = "8000" + } + image_identifier = "public.ecr.aws/aws-containers/hello-app-runner:latest" + image_repository_type = "ECR_PUBLIC" + } + auto_deployments_enabled = false + } + + tags = { + Name = var.resource_name + } +} + +output "resource_aka" { + value = aws_apprunner_service.named_test_resource.arn +} + +output "resource_id" { + value = aws_apprunner_service.named_test_resource.service_id +} + +output "account_id" { + value = data.aws_caller_identity.current.account_id +} + +output "aws_region" { + value = data.aws_region.primary.name +} + +output "resource_name" { + value = var.resource_name +} diff --git a/aws/plugin.go b/aws/plugin.go index fbf0a5917..25d4442c4 100644 --- a/aws/plugin.go +++ b/aws/plugin.go @@ -95,6 +95,7 @@ func Plugin(ctx context.Context) *plugin.Plugin { "aws_api_gatewayv2_integration": tableAwsAPIGatewayV2Integration(ctx), "aws_api_gatewayv2_route": tableAwsAPIGatewayV2Route(ctx), "aws_api_gatewayv2_stage": tableAwsAPIGatewayV2Stage(ctx), + "aws_app_runner_service": tableAwsAppRunnerService(ctx), "aws_appautoscaling_policy": tableAwsAppAutoScalingPolicy(ctx), "aws_appautoscaling_target": tableAwsAppAutoScalingTarget(ctx), "aws_appconfig_application": tableAwsAppConfigApplication(ctx), diff --git a/aws/service.go b/aws/service.go index 7418758c8..c66ec8360 100644 --- a/aws/service.go +++ b/aws/service.go @@ -26,6 +26,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigatewayv2" "github.com/aws/aws-sdk-go-v2/service/appconfig" "github.com/aws/aws-sdk-go-v2/service/applicationautoscaling" + "github.com/aws/aws-sdk-go-v2/service/apprunner" "github.com/aws/aws-sdk-go-v2/service/appstream" "github.com/aws/aws-sdk-go-v2/service/appsync" "github.com/aws/aws-sdk-go-v2/service/athena" @@ -149,6 +150,7 @@ import ( amplifyEndpoint "github.com/aws/aws-sdk-go/service/amplify" apigatewayv2Endpoint "github.com/aws/aws-sdk-go/service/apigatewayv2" + appRunnerEndpoint "github.com/aws/aws-sdk-go/service/apprunner" appsyncv2Endpoint "github.com/aws/aws-sdk-go/service/appsync" auditmanagerEndpoint "github.com/aws/aws-sdk-go/service/auditmanager" backupEndpoint "github.com/aws/aws-sdk-go/service/backup" @@ -313,6 +315,17 @@ func ApplicationAutoScalingClient(ctx context.Context, d *plugin.QueryData) (*ap return applicationautoscaling.NewFromConfig(*cfg), nil } +func AppRunnerClient(ctx context.Context, d *plugin.QueryData) (*apprunner.Client, error) { + cfg, err := getClientForQuerySupportedRegion(ctx, d, appRunnerEndpoint.EndpointsID) + if err != nil { + return nil, err + } + if cfg == nil { + return nil, nil + } + return apprunner.NewFromConfig(*cfg), nil +} + func AppStreamClient(ctx context.Context, d *plugin.QueryData) (*appstream.Client, error) { cfg, err := getClientForQueryRegion(ctx, d) if err != nil { diff --git a/aws/table_aws_app_runner_Service.go b/aws/table_aws_app_runner_Service.go new file mode 100644 index 000000000..be04941b3 --- /dev/null +++ b/aws/table_aws_app_runner_Service.go @@ -0,0 +1,233 @@ +package aws + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/apprunner" + "github.com/aws/aws-sdk-go-v2/service/apprunner/types" + + apprunnerv1 "github.com/aws/aws-sdk-go/service/apprunner" + + "github.com/turbot/steampipe-plugin-sdk/v5/grpc/proto" + "github.com/turbot/steampipe-plugin-sdk/v5/plugin" + "github.com/turbot/steampipe-plugin-sdk/v5/plugin/transform" +) + +//// TABLE DEFINITION + +func tableAwsAppRunnerService(_ context.Context) *plugin.Table { + return &plugin.Table{ + Name: "aws_app_runner_service", + Description: "AWS App Runner Service", + Get: &plugin.GetConfig{ + Hydrate: getAwsAppRunnerService, + KeyColumns: plugin.SingleColumn("arn"), + // We need to handle the InvalidParameterException, as the provided ARN triggers an InvalidRequestException in regions where the resource is unavailable. + IgnoreConfig: &plugin.IgnoreConfig{ + ShouldIgnoreErrorFunc: shouldIgnoreErrors([]string{"ResourceNotFoundException", "InvalidRequestException"}), + }, + Tags: map[string]string{"service": "apprunner", "action": "DescribeService"}, + }, + List: &plugin.ListConfig{ + Hydrate: listAwsAppRunnerServices, + Tags: map[string]string{"service": "apprunner", "action": "ListServices"}, + }, + GetMatrixItemFunc: SupportedRegionMatrix(apprunnerv1.EndpointsID), + Columns: awsRegionalColumns([]*plugin.Column{ + { + Name: "service_name", + Description: "The customer-provided service name.", + Type: proto.ColumnType_STRING, + }, + { + Name: "service_id", + Description: "An ID that App Runner generated for this service.", + Type: proto.ColumnType_STRING, + }, + { + Name: "arn", + Description: "The Amazon Resource Name (ARN) of this service.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("ServiceArn"), + }, + { + Name: "created_at", + Description: "The time when the App Runner service was created. It's in the Unix time stamp format.", + Type: proto.ColumnType_TIMESTAMP, + }, + { + Name: "service_url", + Description: "A subdomain URL that App Runner generated for this service.", + Type: proto.ColumnType_STRING, + }, + { + Name: "updated_at", + Description: "The time when the App Runner service was last updated.", + Type: proto.ColumnType_TIMESTAMP, + }, + { + Name: "deleted_at", + Description: "The time when the App Runner service was deleted.", + Type: proto.ColumnType_TIMESTAMP, + Hydrate: getAwsAppRunnerService, + Transform: transform.FromField("DeletedAt").Transform(transform.NullIfZeroValue), + }, + { + Name: "kms_key", + Description: "The ARN of the KMS key that's used for encryption.", + Type: proto.ColumnType_STRING, + Hydrate: getAwsAppRunnerService, + Transform: transform.FromField("EncryptionConfiguration.KmsKey"), + }, + { + Name: "policy_type", + Description: "The policy type. Currently supported values are TargetTrackingScaling and StepScaling", + Type: proto.ColumnType_STRING, + }, + { + Name: "auto_scaling_configuration_summary", + Description: "Summary information for the App Runner automatic scaling configuration resource that's associated with this service.", + Type: proto.ColumnType_JSON, + Hydrate: getAwsAppRunnerService, + }, + { + Name: "instance_configuration", + Description: "The runtime configuration of instances (scaling units) of this service.", + Type: proto.ColumnType_JSON, + Hydrate: getAwsAppRunnerService, + }, + { + Name: "network_configuration", + Description: "Configuration settings related to network traffic of the web application that this service runs.", + Type: proto.ColumnType_JSON, + Hydrate: getAwsAppRunnerService, + }, + { + Name: "source_configuration", + Description: "The source deployed to the App Runner service. It can be a code or an image repository.", + Type: proto.ColumnType_JSON, + Hydrate: getAwsAppRunnerService, + }, + { + Name: "health_check_configuration", + Description: "The settings for the health check that App Runner performs to monitor the health of this service.", + Type: proto.ColumnType_JSON, + Hydrate: getAwsAppRunnerService, + }, + { + Name: "observability_configuration", + Description: "The observability configuration of this service.", + Type: proto.ColumnType_JSON, + Hydrate: getAwsAppRunnerService, + }, + + // Standard standard columns + { + Name: "title", + Description: resourceInterfaceDescription("title"), + Type: proto.ColumnType_STRING, + Transform: transform.FromField("ServiceName"), + }, + { + Name: "akas", + Description: resourceInterfaceDescription("akas"), + Type: proto.ColumnType_JSON, + Transform: transform.FromField("ServiceArn").Transform(transform.EnsureStringArray), + }, + }), + } +} + +//// LIST FUNCTION + +func listAwsAppRunnerServices(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) { + // Create Session + svc, err := AppRunnerClient(ctx, d) + if err != nil { + plugin.Logger(ctx).Error("aws_app_runner_service.listAwsAppRunnerServices", "client_error", err) + return nil, err + } + + if svc == nil { + return nil, nil // Unsupported region check + } + + // Limit the result + input := &apprunner.ListServicesInput{ + MaxResults: aws.Int32(20), + } + + if d.QueryContext.Limit != nil { + limit := int32(*d.QueryContext.Limit) + if limit < *input.MaxResults { + if limit < 1 { + input.MaxResults = aws.Int32(1) + } else { + input.MaxResults = aws.Int32(limit) + } + } + } + + paginator := apprunner.NewListServicesPaginator(svc, input, func(o *apprunner.ListServicesPaginatorOptions) { + o.Limit = *input.MaxResults + o.StopOnDuplicateToken = true + }) + + for paginator.HasMorePages() { + // apply rate limiting + d.WaitForListRateLimit(ctx) + + output, err := paginator.NextPage(ctx) + if err != nil { + plugin.Logger(ctx).Error("aws_app_runner_service.listAwsAppRunnerServices", "api_error", err) + return nil, err + } + + for _, service := range output.ServiceSummaryList { + d.StreamListItem(ctx, service) + + // Context can be cancelled due to manual cancellation or the limit has been hit + if d.RowsRemaining(ctx) == 0 { + return nil, nil + } + } + } + + return nil, err +} + +//// HYDRATE FUNCTIONS + +func getAwsAppRunnerService(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + + // Create session + svc, err := AppRunnerClient(ctx, d) + if err != nil { + plugin.Logger(ctx).Error("aws_app_runner_service.getAwsAppRunnerService", "client_error", err) + return nil, err + } + + if svc == nil { + return nil, nil // Unsupported region check + } + + var arn string + if h.Item != nil { + arn = *h.Item.(types.ServiceSummary).ServiceArn + } else { + arn = d.EqualsQuals["arn"].GetStringValue() + } + + params := &apprunner.DescribeServiceInput{ + ServiceArn: aws.String(arn), + } + + service, err := svc.DescribeService(ctx, params) + if err != nil { + plugin.Logger(ctx).Error("aws_app_runner_service.getAwsAppRunnerService", "api_error", err) + return nil, err + } + + return service.Service, nil +} diff --git a/docs/tables/aws_app_runner_service.md b/docs/tables/aws_app_runner_service.md new file mode 100644 index 000000000..3eaceb442 --- /dev/null +++ b/docs/tables/aws_app_runner_service.md @@ -0,0 +1,160 @@ +--- +title: "Steampipe Table: aws_app_runner_service - Query AWS App Runner Services using SQL" +description: "Allows users to query AWS App Runner services, providing detailed information on service configurations, scaling, and network settings." +--- + +# Table: aws_app_runner_service - Query AWS App Runner Services using SQL + +AWS App Runner is a fully managed service that makes it easy to build, deploy, and run containerized web applications and APIs at scale. The `aws_app_runner_service` table in Steampipe allows you to query information about your App Runner services in AWS, including their configurations, scaling policies, and network settings. + +## Table Usage Guide + +The `aws_app_runner_service` table enables cloud administrators and DevOps engineers to gather detailed insights into their App Runner services. You can query various aspects of the services, such as their scaling configurations, network settings, health checks, and service URLs. This table is particularly useful for monitoring service health, managing configurations, and ensuring that your applications are running efficiently. + +## Examples + +### Basic service information +Retrieve basic information about your AWS App Runner services, including their name, ARN, and region. + +```sql+postgres +select + service_name, + arn, + region, + created_at, + updated_at +from + aws_app_runner_service; +``` + +```sql+sqlite +select + service_name, + arn, + region, + created_at, + updated_at +from + aws_app_runner_service; +``` + +### List services with specific network configurations +Identify services that are configured with a specific network configuration, such as VPC or public network settings. + +```sql+postgres +select + service_name, + arn, + network_configuration +from + aws_app_runner_service +where + (network_configuration -> 'EgressConfiguration' ->> 'VpcConnectorArn') is not null; +``` + +```sql+sqlite +select + service_name, + arn, + network_configuration +from + aws_app_runner_service +where + json_extract(network_configuration, '$.VpcConfiguration') is not null; +``` + +### List services with auto-scaling configurations +Retrieve information about services that have specific auto-scaling configurations. + +```sql+postgres +select + service_name, + arn, + auto_scaling_configuration_summary +from + aws_app_runner_service +where + jsonb_path_exists(auto_scaling_configuration_summary, '$.AutoScalingConfigurationArn'); +``` + +```sql+sqlite +select + service_name, + arn, + auto_scaling_configuration_summary +from + aws_app_runner_service +where + json_extract(auto_scaling_configuration_summary, '$.AutoScalingConfigurationArn') is not null; +``` + +### List services with specific observability configurations +Identify services that have observability features enabled, such as logging or tracing. + +```sql+postgres +select + service_name, + arn, + observability_configuration +from + aws_app_runner_service +where + (observability_configuration ->> 'ObservabilityConfigurationArn') is not null; +``` + +```sql+sqlite +select + service_name, + arn, + observability_configuration +from + aws_app_runner_service +where + json_extract(observability_configuration, '$.ObservabilityConfigurationArn') is not null; +``` + +### List services created within a specific time frame +Fetch services that were created within a specific date range, which can be useful for auditing purposes. + +```sql+postgres +select + service_name, + arn, + created_at +from + aws_app_runner_service +where + created_at >= '2023-01-01T00:00:00Z' and created_at <= '2023-12-31T23:59:59Z'; +``` + +```sql+sqlite +select + service_name, + arn, + created_at +from + aws_app_runner_service +where + created_at >= '2023-01-01T00:00:00Z' and created_at <= '2023-12-31T23:59:59Z'; +``` + +### Get service URLs for all services +Retrieve the service URLs for all App Runner services, which can be useful for accessing or sharing service endpoints. + +```sql+postgres +select + service_name, + arn, + service_url +from + aws_app_runner_service; +``` + +```sql+sqlite +select + service_name, + arn, + service_url +from + aws_app_runner_service; +``` \ No newline at end of file diff --git a/go.mod b/go.mod index a97895385..c4e6a6c0a 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.20.4 github.com/aws/aws-sdk-go-v2/service/appconfig v1.29.2 github.com/aws/aws-sdk-go-v2/service/applicationautoscaling v1.27.4 + github.com/aws/aws-sdk-go-v2/service/apprunner v1.28.8 github.com/aws/aws-sdk-go-v2/service/appstream v1.34.4 github.com/aws/aws-sdk-go-v2/service/appsync v1.31.4 github.com/aws/aws-sdk-go-v2/service/athena v1.40.4 diff --git a/go.sum b/go.sum index d42e735b1..7ae6e4e2a 100644 --- a/go.sum +++ b/go.sum @@ -243,6 +243,8 @@ github.com/aws/aws-sdk-go-v2/service/appconfig v1.29.2 h1:Nm1Pqug23c/Ib+/FgwYpFZ github.com/aws/aws-sdk-go-v2/service/appconfig v1.29.2/go.mod h1:Z4uxjsQCQYIZQYOf5js8AN9B5ZCFfwRkEHuiihgjHWs= github.com/aws/aws-sdk-go-v2/service/applicationautoscaling v1.27.4 h1:QGG9y+wEdP5KpTbcvpi8ETAoMq0zB6UJdqJ3JmVu/Wc= github.com/aws/aws-sdk-go-v2/service/applicationautoscaling v1.27.4/go.mod h1:g7O+8ghAn49ysZShSpeOxIRiI0/BgPoqHwZFNKnykco= +github.com/aws/aws-sdk-go-v2/service/apprunner v1.28.8 h1:vTSRA431Gi6tQcUDfCTF1PwnLvw7M+7SoMWb0FRvKAY= +github.com/aws/aws-sdk-go-v2/service/apprunner v1.28.8/go.mod h1:0ClIRoMxROYgDXb/kSvAsZSO41p4j9p4xkquAFzNEjM= github.com/aws/aws-sdk-go-v2/service/appstream v1.34.4 h1:chEtg7jpLbd+wzNEZR5Y7if5S3+zCL4HO892dk4JRHI= github.com/aws/aws-sdk-go-v2/service/appstream v1.34.4/go.mod h1:ornvkYF5+PhIhj13BZGWGlZyltIkYqfoPtmAnOdSORA= github.com/aws/aws-sdk-go-v2/service/appsync v1.31.4 h1:E6Lgar42LVTsQPOQ+1UDjItIliPnm2/R8jnBA6fo6Gg=