diff --git a/.gitignore b/.gitignore index 8b978ac..f48d898 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,36 @@ .DS_Store -*.DS_Store + +# Local .terraform directories +**/.terraform/* + +# Terraform lockfile +.terraform.lock.hcl + +# .tfstate files +*.tfstate +*.tfstate.* +*.tfplan + +# Crash log files +crash.log + +# Exclude all .tfvars files, which are likely to contain sentitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +!example.terraform.tfvars + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Ignore CLI configuration files +.terraformrc +terraform.rc + +# Zip files generated +*.zip diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a0925b0 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,29 @@ +repos: + - repo: https://github.com/antonbabenko/pre-commit-terraform + rev: v1.68.1 + hooks: + - id: terraform_fmt + - id: terraform_validate + - id: terraform_docs + args: + - '--args=--lockfile=false' + - id: terraform_tflint + args: + - '--args=--only=terraform_deprecated_interpolation' + - '--args=--only=terraform_deprecated_index' + - '--args=--only=terraform_unused_declarations' + - '--args=--only=terraform_comment_syntax' + - '--args=--only=terraform_documented_outputs' + - '--args=--only=terraform_documented_variables' + - '--args=--only=terraform_typed_variables' + - '--args=--only=terraform_module_pinned_source' + - '--args=--only=terraform_naming_convention' + - '--args=--only=terraform_required_version' + - '--args=--only=terraform_required_providers' + - '--args=--only=terraform_standard_module_structure' + - '--args=--only=terraform_workspace_remote' + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.2.0 + hooks: + - id: check-merge-conflict + - id: end-of-file-fixer diff --git a/LICENSE b/LICENSE index 1bb4f21..6aa0c45 100644 --- a/LICENSE +++ b/LICENSE @@ -12,4 +12,3 @@ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/README.md b/README.md index 0de100c..332c4fa 100644 --- a/README.md +++ b/README.md @@ -38,4 +38,3 @@ See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more inform ## License This library is licensed under the MIT-0 License. See the LICENSE file. - diff --git a/lambda-vpc/sample-lambda-function-template/v1/instance_infrastructure/lambda.tf b/lambda-vpc/sample-lambda-function-template/v1/instance_infrastructure/lambda.tf index 35eff65..2e08201 100644 --- a/lambda-vpc/sample-lambda-function-template/v1/instance_infrastructure/lambda.tf +++ b/lambda-vpc/sample-lambda-function-template/v1/instance_infrastructure/lambda.tf @@ -34,4 +34,4 @@ resource "aws_lambda_function" "my_lambda" { subnet_ids = [var.environment.outputs.subnet_id] security_group_ids = [var.environment.outputs.security_group_id] } -} \ No newline at end of file +} diff --git a/lambda-vpc/sample-lambda-function-template/v1/instance_infrastructure/manifest.yaml b/lambda-vpc/sample-lambda-function-template/v1/instance_infrastructure/manifest.yaml index 8d90345..f0d8bf1 100644 --- a/lambda-vpc/sample-lambda-function-template/v1/instance_infrastructure/manifest.yaml +++ b/lambda-vpc/sample-lambda-function-template/v1/instance_infrastructure/manifest.yaml @@ -2,4 +2,4 @@ infrastructure: templates: - file: "*" rendering_engine: hcl - template_language: terraform \ No newline at end of file + template_language: terraform diff --git a/lambda-vpc/sample-lambda-function-template/v1/instance_infrastructure/outputs.tf b/lambda-vpc/sample-lambda-function-template/v1/instance_infrastructure/outputs.tf index cee0ca9..8374208 100644 --- a/lambda-vpc/sample-lambda-function-template/v1/instance_infrastructure/outputs.tf +++ b/lambda-vpc/sample-lambda-function-template/v1/instance_infrastructure/outputs.tf @@ -1,3 +1,3 @@ output "lambda_arn" { value = "whocares" -} \ No newline at end of file +} diff --git a/lambda-vpc/sample-lambda-function-template/v1/schema/schema.yaml b/lambda-vpc/sample-lambda-function-template/v1/schema/schema.yaml index 37a5817..5b34525 100644 --- a/lambda-vpc/sample-lambda-function-template/v1/schema/schema.yaml +++ b/lambda-vpc/sample-lambda-function-template/v1/schema/schema.yaml @@ -31,4 +31,4 @@ schema: type: string description: "The s3 key where the function code is stored" minLength: 1 - maxLength: 200 \ No newline at end of file + maxLength: 200 diff --git a/lambda-vpc/sample-vpc-environment-template/v1/infrastructure/outputs.tf b/lambda-vpc/sample-vpc-environment-template/v1/infrastructure/outputs.tf index 57c45e5..769e383 100644 --- a/lambda-vpc/sample-vpc-environment-template/v1/infrastructure/outputs.tf +++ b/lambda-vpc/sample-vpc-environment-template/v1/infrastructure/outputs.tf @@ -8,4 +8,4 @@ output "subnet_id" { output "security_group_id" { value = module.vpc.default_security_group_id -} \ No newline at end of file +} diff --git a/lambda-vpc/sample-vpc-environment-template/v1/infrastructure/vpc.tf b/lambda-vpc/sample-vpc-environment-template/v1/infrastructure/vpc.tf index 266c040..7df9c91 100644 --- a/lambda-vpc/sample-vpc-environment-template/v1/infrastructure/vpc.tf +++ b/lambda-vpc/sample-vpc-environment-template/v1/infrastructure/vpc.tf @@ -16,4 +16,3 @@ module "vpc" { Environment = var.environment.name } } - diff --git a/service-templates/apigw-lambda-svc/dev-resources/proton.auto.tfvars.json b/service-templates/apigw-lambda-svc/dev-resources/proton.auto.tfvars.json deleted file mode 100644 index 955bd2f..0000000 --- a/service-templates/apigw-lambda-svc/dev-resources/proton.auto.tfvars.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "service": { - "name": "REPLACE_ME", - "inputs": { - }, - "repository_connection_arn": "REPLACE_ME", - "repository_id": "REPLACE_ME", - "branch_name": "REPLACE_ME" - }, - "service_instances": [ - { - "name": "REPLACE_ME", - "inputs": { - }, - "outputs": { - "LambdaRuntime": "nodejs12.x" - }, - "environment": { - "account_id": "", - "name": "", - "outputs": {} - } - } - ], - "pipeline": { - "inputs": { - "environment_account_ids": "", - "code_dir": "lambda-ping-sns", - "unit_test_command": "echo 'add your unit test command here'", - "packaging_command": "zip function.zip app.js" - } - }, - "environment": { - "outputs": { - "SnsTopicArn": "REPLACE_ME", - "SnsTopicName": "REPLACE_ME" - } - }, - "service_instance": { - "name": "REPLACE_ME", - "inputs": { - "lambda_runtime": "nodejs12.x", - "lambda_handler": "app.handler", - "lambda_bucket": "REPLACE_ME", - "lambda_key": "REPLACE_ME" - } - } -} diff --git a/service-templates/apigw-lambda-svc/dev-resources/proton.variables.tf b/service-templates/apigw-lambda-svc/dev-resources/proton.variables.tf deleted file mode 100644 index da7eb91..0000000 --- a/service-templates/apigw-lambda-svc/dev-resources/proton.variables.tf +++ /dev/null @@ -1,47 +0,0 @@ - -variable "service_instances" { - type = list( - object({ - name = string - inputs = map(string) - outputs = map(string) - environment = object({ - account_id = string - name = string - outputs = map(string) - }) - }) - ) -} - -variable "service_instance" { - type = object({ - name = string - inputs = map(string) - }) - default = null -} - -variable "service" { - type = object({ - name = string - repository_id = string - repository_connection_arn = string - branch_name = string - }) - default = null -} - -variable "environment" { - type = object({ - outputs = map(string) - }) - default = null -} - -variable "pipeline" { - type = object({ - inputs = map(string) - }) - default = null -} \ No newline at end of file diff --git a/service-templates/apigw-lambda-svc/v1/instance_infrastructure/README.md b/service-templates/apigw-lambda-svc/v1/instance_infrastructure/README.md new file mode 100644 index 0000000..f7ffb8b --- /dev/null +++ b/service-templates/apigw-lambda-svc/v1/instance_infrastructure/README.md @@ -0,0 +1,78 @@ +# Instance + +## Local Development & Testing + +Copy the provided `example.terraform.tfvars` locally and rename to `terraform.tfvars`. + +Add, update, modify any variables required for testing within the `terraform.tfvars` file. + +Provision the resources using: + +```bash +$ terraform init +$ terraform plan +$ terraform apply +``` +Note that this example may create resources which cost money. Run `terraform destroy` when you no longer need these resources. + +``` + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 0.13.1 | +| [archive](#requirement\_archive) | ~> 2.0 | +| [aws](#requirement\_aws) | ~> 4.0 | + +## Providers + +| Name | Version | +|------|---------| +| [archive](#provider\_archive) | ~> 2.0 | +| [aws](#provider\_aws) | ~> 4.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_apigatewayv2_api.lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_api) | resource | +| [aws_apigatewayv2_integration.lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_integration) | resource | +| [aws_apigatewayv2_route.function](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_route) | resource | +| [aws_apigatewayv2_stage.lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_stage) | resource | +| [aws_cloudwatch_log_group.api_gw](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | +| [aws_iam_policy.sns](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_role.function](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy_attachment.lambda_basic_execution](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.sns](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_lambda_function.function](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource | +| [aws_lambda_permission.function](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission) | resource | +| [archive_file.function](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | +| [aws_iam_policy_document.sns](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [aws\_region](#input\_aws\_region) | Region where resources will be provisioned | `string` | `"us-east-1"` | no | +| [lambda\_function\_name](#input\_lambda\_function\_name) | Name of the Lambda function | `string` | `"function"` | no | +| [lambda\_handler](#input\_lambda\_handler) | Handler of the Lambda function | `string` | `"app.handler"` | no | +| [lambda\_runtime](#input\_lambda\_runtime) | Runtime of the Lambda function | `string` | `"nodejs12.x"` | no | +| [lambda\_s3\_bucket](#input\_lambda\_s3\_bucket) | S3 bucket where Lambda function code is stored | `string` | `""` | no | +| [lambda\_s3\_key](#input\_lambda\_s3\_key) | S3 key where Lambda function code is stored | `string` | `""` | no | +| [service\_name](#input\_service\_name) | Name of the service | `string` | `"apigw-lambda-svc"` | no | +| [sns\_topic\_arn](#input\_sns\_topic\_arn) | SNS topic ARN | `string` | `""` | no | +| [sns\_topic\_name](#input\_sns\_topic\_name) | Name of the SNS topic | `string` | `""` | no | +| [use\_local\_function](#input\_use\_local\_function) | Determines whether to use a local function archive (`true`) or external stored in S3 (`false`) | `bool` | `true` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [HttpApiEndpoint](#output\_HttpApiEndpoint) | The default endpoint for the HTTP API | +| [LambdaRuntime](#output\_LambdaRuntime) | The runtime of the Lambda function | + diff --git a/service-templates/apigw-lambda-svc/v1/instance_infrastructure/config.tf b/service-templates/apigw-lambda-svc/v1/instance_infrastructure/config.tf deleted file mode 100644 index 68e3646..0000000 --- a/service-templates/apigw-lambda-svc/v1/instance_infrastructure/config.tf +++ /dev/null @@ -1,27 +0,0 @@ -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - version = "~> 3.0" - } - } - - backend "s3" {} -} - -# Configure the AWS Provider -provider "aws" { - region = var.aws_region - alias = "default" - - default_tags { - tags = { - "proton:service" = var.service.name - } - } -} - -variable "aws_region" { - type = string - default = "us-east-1" -} \ No newline at end of file diff --git a/service-templates/apigw-lambda-svc/v1/instance_infrastructure/data.tf b/service-templates/apigw-lambda-svc/v1/instance_infrastructure/data.tf deleted file mode 100644 index 454de88..0000000 --- a/service-templates/apigw-lambda-svc/v1/instance_infrastructure/data.tf +++ /dev/null @@ -1,47 +0,0 @@ -data "aws_region" "current" {} - -data "aws_caller_identity" "current" {} - -data "aws_partition" "current" {} - -data "aws_iam_policy_document" "sns_publish_policy_document" { - statement { - actions = [ - "sns:Publish" - ] - resources = [ - var.environment.outputs.SnsTopicArn - ] - } -} - -data "archive_file" "lambda_zip_inline" { - type = "zip" - output_path = "lambda_zip_inline.zip" - - source { - filename = "index.js" - content = < { - try { - // Log event and context object to CloudWatch Logs - console.log("Event: ", JSON.stringify(event, null, 2)); - console.log("Context: ", JSON.stringify(context, null, 2)); - // Create event object to return to caller - const eventObj = { - functionName: context.functionName, - rawPath: event.rawPath, - }; - const response = { - statusCode: 200, - body: JSON.stringify(eventObj, null, 2), - }; - return response; - } catch (error) { - console.error(error); - throw new Error(error); - } - }; -EOF - } -} \ No newline at end of file diff --git a/service-templates/apigw-lambda-svc/v1/instance_infrastructure/example.terraform.tfvars b/service-templates/apigw-lambda-svc/v1/instance_infrastructure/example.terraform.tfvars new file mode 100644 index 0000000..dae52f8 --- /dev/null +++ b/service-templates/apigw-lambda-svc/v1/instance_infrastructure/example.terraform.tfvars @@ -0,0 +1,6 @@ +# Copy+paste and rename to `terraform.tfvars` for working locally +service_name = "terraform-test-service" +lambda_function_name = "terraform-test-function" + +sns_topic_name = "terraform-test-topic" +sns_topic_arn = "aws:sns:us-east-1:123456789012:${var.sns_topic_name}" diff --git a/service-templates/apigw-lambda-svc/v1/instance_infrastructure/instance.tf b/service-templates/apigw-lambda-svc/v1/instance_infrastructure/instance.tf new file mode 100644 index 0000000..76ff774 --- /dev/null +++ b/service-templates/apigw-lambda-svc/v1/instance_infrastructure/instance.tf @@ -0,0 +1,147 @@ +################################################################################ +# HTTI API +################################################################################ + +resource "aws_apigatewayv2_api" "lambda" { + name = var.service_name + protocol_type = "HTTP" + + cors_configuration { + allow_origins = ["*"] + allow_methods = [ + "GET", + "HEAD", + "OPTIONS", + "POST", + ] + } +} + +resource "aws_apigatewayv2_stage" "lambda" { + api_id = aws_apigatewayv2_api.lambda.id + + name = "serverless_lambda_stage" + auto_deploy = true + + access_log_settings { + destination_arn = aws_cloudwatch_log_group.api_gw.arn + + format = jsonencode({ + requestId = "$context.requestId" + sourceIp = "$context.identity.sourceIp" + requestTime = "$context.requestTime" + protocol = "$context.protocol" + httpMethod = "$context.httpMethod" + resourcePath = "$context.resourcePath" + routeKey = "$context.routeKey" + status = "$context.status" + responseLength = "$context.responseLength" + integrationErrorMessage = "$context.integrationErrorMessage" + }) + } +} + +resource "aws_cloudwatch_log_group" "api_gw" { + name = "/aws/api_gw/${aws_apigatewayv2_api.lambda.name}" + + retention_in_days = 30 +} + +################################################################################ +# HTTP Integration & Routes +################################################################################ + +resource "aws_apigatewayv2_integration" "lambda" { + api_id = aws_apigatewayv2_api.lambda.id + + integration_uri = aws_lambda_function.function.invoke_arn + integration_type = "AWS_PROXY" + integration_method = "POST" +} + +resource "aws_apigatewayv2_route" "function" { + api_id = aws_apigatewayv2_api.lambda.id + + route_key = "$default" + target = "integrations/${aws_apigatewayv2_integration.lambda.id}" +} + +################################################################################ +# Lambda Function +################################################################################ + +data "archive_file" "function" { + count = var.use_local_function ? 1 : 0 + + type = "zip" + output_path = "${path.module}/function.zip" + + source { + filename = "index.js" + content = file("${path.module}/lambdas/function.js") + } +} + +resource "aws_lambda_function" "function" { + function_name = var.lambda_function_name + runtime = var.lambda_runtime + role = aws_iam_role.function.arn + + environment { + variables = { + SnsTopicName = var.sns_topic_name + } + } + + handler = var.lambda_handler + s3_bucket = var.use_local_function ? null : var.lambda_s3_bucket + s3_key = var.use_local_function ? null : var.lambda_s3_key + filename = var.use_local_function ? data.archive_file.function[0].output_path : null +} + +resource "aws_lambda_permission" "function" { + source_arn = "${aws_apigatewayv2_api.lambda.execution_arn}/*/*" + + statement_id = "AllowExecutionFromAPIGateway" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.function.function_name + principal = "apigateway.amazonaws.com" +} + +resource "aws_iam_role" "function" { + name_prefix = "serverless_lambda" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "lambda.amazonaws.com" + } + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "lambda_basic_execution" { + role = aws_iam_role.function.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" +} + +data "aws_iam_policy_document" "sns" { + statement { + actions = ["sns:Publish"] + resources = [var.sns_topic_arn] + } +} + +resource "aws_iam_policy" "sns" { + policy = data.aws_iam_policy_document.sns.json +} + +resource "aws_iam_role_policy_attachment" "sns" { + role = aws_iam_role.function.name + policy_arn = aws_iam_policy.sns.arn +} diff --git a/service-templates/apigw-lambda-svc/v1/instance_infrastructure/lambdas/function.js b/service-templates/apigw-lambda-svc/v1/instance_infrastructure/lambdas/function.js new file mode 100644 index 0000000..4d8a280 --- /dev/null +++ b/service-templates/apigw-lambda-svc/v1/instance_infrastructure/lambdas/function.js @@ -0,0 +1,24 @@ +exports.handler = async (event, context) => { + try { + // Log event and context object to CloudWatch Logs + console.log("Event: ", JSON.stringify(event, null, 2)); + console.log("Context: ", JSON.stringify(context, null, 2)); + + // Create event object to return to caller + const eventObj = { + functionName: context.functionName, + rawPath: event.rawPath, + }; + + const response = { + statusCode: 200, + body: JSON.stringify(eventObj, null, 2), + }; + + return response; + + } catch (error) { + console.error(error); + throw new Error(error); + } +}; diff --git a/service-templates/apigw-lambda-svc/v1/instance_infrastructure/main.tf b/service-templates/apigw-lambda-svc/v1/instance_infrastructure/main.tf index b915d15..3a44751 100644 --- a/service-templates/apigw-lambda-svc/v1/instance_infrastructure/main.tf +++ b/service-templates/apigw-lambda-svc/v1/instance_infrastructure/main.tf @@ -1,117 +1,30 @@ -resource "aws_apigatewayv2_api" "lambda" { - name = var.service.name - protocol_type = "HTTP" - cors_configuration { - allow_origins = ["*"] - allow_methods = [ - "GET", - "HEAD", - "OPTIONS", - "POST", - ] - } - - # target = aws_lambda_function.lambda_function.arn -} - -resource "aws_apigatewayv2_stage" "lambda" { - api_id = aws_apigatewayv2_api.lambda.id +################################################################################ +# Terraform Backend & Provider +################################################################################ - name = "serverless_lambda_stage" - auto_deploy = true +terraform { + required_version = ">= 0.13.1" - access_log_settings { - destination_arn = aws_cloudwatch_log_group.api_gw.arn - - format = jsonencode({ - requestId = "$context.requestId" - sourceIp = "$context.identity.sourceIp" - requestTime = "$context.requestTime" - protocol = "$context.protocol" - httpMethod = "$context.httpMethod" - resourcePath = "$context.resourcePath" - routeKey = "$context.routeKey" - status = "$context.status" - responseLength = "$context.responseLength" - integrationErrorMessage = "$context.integrationErrorMessage" - }) + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 4.0" + } + archive = { + source = "hashicorp/archive" + version = "~> 2.0" + } } -} - -resource "aws_cloudwatch_log_group" "api_gw" { - name = "/aws/api_gw/${aws_apigatewayv2_api.lambda.name}" - retention_in_days = 30 + backend "s3" {} } -resource "aws_lambda_function" "lambda_function" { - function_name = "${var.service_instance.name}-function" - runtime = var.service_instance.inputs.lambda_runtime - role = aws_iam_role.lambda_exec.arn +provider "aws" { + region = var.aws_region - environment { - variables = { - SnsTopicName = var.environment.outputs.SnsTopicName + default_tags { + tags = { + "proton:pipeline" = var.service_name } } - - handler = contains(keys(var.service_instance.inputs), "lambda_bucket") ? var.service_instance.inputs.lambda_handler : "index.handler" - s3_bucket = contains(keys(var.service_instance.inputs), "lambda_bucket") ? var.service_instance.inputs.lambda_bucket : null - s3_key = contains(keys(var.service_instance.inputs), "lambda_bucket") ? var.service_instance.inputs.lambda_key : null - filename = contains(keys(var.service_instance.inputs), "lambda_bucket") ? null : data.archive_file.lambda_zip_inline.output_path } - -resource "aws_apigatewayv2_integration" "lambda_integration" { - api_id = aws_apigatewayv2_api.lambda.id - - integration_uri = aws_lambda_function.lambda_function.invoke_arn - integration_type = "AWS_PROXY" - integration_method = "POST" -} - -resource "aws_apigatewayv2_route" "hello_world" { - api_id = aws_apigatewayv2_api.lambda.id - - route_key = "$default" - target = "integrations/${aws_apigatewayv2_integration.lambda_integration.id}" -} - -resource "aws_lambda_permission" "api_gw" { - statement_id = "AllowExecutionFromAPIGateway" - action = "lambda:InvokeFunction" - function_name = aws_lambda_function.lambda_function.function_name - principal = "apigateway.amazonaws.com" - - source_arn = "${aws_apigatewayv2_api.lambda.execution_arn}/*/*" -} - -resource "aws_iam_role" "lambda_exec" { - name_prefix = "serverless_lambda" - - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Action = "sts:AssumeRole" - Effect = "Allow" - Principal = { - Service = "lambda.amazonaws.com" - } - } - ] - }) -} - -resource "aws_iam_role_policy_attachment" "lambda_exec_policy" { - role = aws_iam_role.lambda_exec.name - policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" -} - -resource "aws_iam_role_policy_attachment" "ssn_publish_policy" { - role = aws_iam_role.lambda_exec.name - policy_arn = aws_iam_policy.sns_publish_policy.arn -} - -resource "aws_iam_policy" "sns_publish_policy" { - policy = data.aws_iam_policy_document.sns_publish_policy_document.json -} \ No newline at end of file diff --git a/service-templates/apigw-lambda-svc/v1/instance_infrastructure/manifest.yaml b/service-templates/apigw-lambda-svc/v1/instance_infrastructure/manifest.yaml index 8d90345..f0d8bf1 100644 --- a/service-templates/apigw-lambda-svc/v1/instance_infrastructure/manifest.yaml +++ b/service-templates/apigw-lambda-svc/v1/instance_infrastructure/manifest.yaml @@ -2,4 +2,4 @@ infrastructure: templates: - file: "*" rendering_engine: hcl - template_language: terraform \ No newline at end of file + template_language: terraform diff --git a/service-templates/apigw-lambda-svc/v1/instance_infrastructure/outputs.tf b/service-templates/apigw-lambda-svc/v1/instance_infrastructure/outputs.tf index bd1a711..f87b446 100644 --- a/service-templates/apigw-lambda-svc/v1/instance_infrastructure/outputs.tf +++ b/service-templates/apigw-lambda-svc/v1/instance_infrastructure/outputs.tf @@ -1,9 +1,11 @@ +# tflint-ignore: terraform_naming_convention - matching case of CloudFormation equivalent for testing purposes output "HttpApiEndpoint" { - description = "The default endpoint for the HTTP API." - - value = aws_apigatewayv2_stage.lambda.invoke_url + description = "The default endpoint for the HTTP API" + value = aws_apigatewayv2_stage.lambda.invoke_url } +# tflint-ignore: terraform_naming_convention - matching case of CloudFormation equivalent for testing purposes output "LambdaRuntime" { - value = var.service_instance.inputs.lambda_runtime -} \ No newline at end of file + description = "The runtime of the Lambda function" + value = var.lambda_runtime +} diff --git a/service-templates/apigw-lambda-svc/v1/instance_infrastructure/variables.tf b/service-templates/apigw-lambda-svc/v1/instance_infrastructure/variables.tf new file mode 100644 index 0000000..0df81f3 --- /dev/null +++ b/service-templates/apigw-lambda-svc/v1/instance_infrastructure/variables.tf @@ -0,0 +1,59 @@ +variable "aws_region" { + description = "Region where resources will be provisioned" + type = string + default = "us-east-1" +} + +variable "service_name" { + description = "Name of the service" + type = string + default = "apigw-lambda-svc" +} + +variable "lambda_function_name" { + description = "Name of the Lambda function" + type = string + default = "function" +} + +variable "lambda_handler" { + description = "Handler of the Lambda function" + type = string + default = "app.handler" +} + +variable "lambda_runtime" { + description = "Runtime of the Lambda function" + type = string + default = "nodejs12.x" +} + +variable "use_local_function" { + description = "Determines whether to use a local function archive (`true`) or external stored in S3 (`false`)" + type = bool + default = true +} + +variable "lambda_s3_bucket" { + description = "S3 bucket where Lambda function code is stored" + type = string + default = "" +} + +variable "lambda_s3_key" { + description = "S3 key where Lambda function code is stored" + type = string + default = "" +} + +variable "sns_topic_name" { + description = "Name of the SNS topic" + type = string + default = "" +} + +variable "sns_topic_arn" { + description = "SNS topic ARN" + type = string + default = "" +} diff --git a/service-templates/apigw-lambda-svc/v1/pipeline_infrastructure/README.md b/service-templates/apigw-lambda-svc/v1/pipeline_infrastructure/README.md new file mode 100644 index 0000000..8f8a7f9 --- /dev/null +++ b/service-templates/apigw-lambda-svc/v1/pipeline_infrastructure/README.md @@ -0,0 +1,93 @@ +# Pipeline + +## Local Development & Testing + +Copy the provided `example.terraform.tfvars` locally and rename to `terraform.tfvars`. + +Add, update, modify any variables required for testing within the `terraform.tfvars` file. + +Provision the resources using: + +```bash +$ terraform init +$ terraform plan +$ terraform apply +``` +Note that this example may create resources which cost money. Run `terraform destroy` when you no longer need these resources. + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 0.13.1 | +| [aws](#requirement\_aws) | ~> 4.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | ~> 4.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_codebuild_project.build](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/codebuild_project) | resource | +| [aws_codebuild_project.deploy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/codebuild_project) | resource | +| [aws_codepipeline.pipeline](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/codepipeline) | resource | +| [aws_iam_policy.build](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.deploy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.pipeline](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.pipeline_action](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_role.build](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role.deploy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role.pipeline](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role.pipeline_action](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy_attachment.build](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.deploy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.pipeline](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.pipeline_action](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_kms_alias.pipeline_artifacts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_alias) | resource | +| [aws_kms_key.pipeline_artifacts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_key) | resource | +| [aws_s3_bucket.function](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource | +| [aws_s3_bucket.pipeline_artifacts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource | +| [aws_s3_bucket_policy.function](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_policy) | resource | +| [aws_s3_bucket_public_access_block.pipeline_artifacts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource | +| [aws_s3_bucket_server_side_encryption_configuration.function](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_server_side_encryption_configuration) | resource | +| [aws_s3_bucket_server_side_encryption_configuration.pipeline_artifacts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_server_side_encryption_configuration) | resource | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_iam_policy_document.build](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.deploy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.function](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.pipeline](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.pipeline_action](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.pipeline_artifacts_kms](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [aws\_region](#input\_aws\_region) | Region where resources will be provisioned | `string` | `"us-east-1"` | no | +| [codebuild\_deployments](#input\_codebuild\_deployments) | CodeBuild deployment projects | `any` | `{}` | no | +| [environment\_account\_ids](#input\_environment\_account\_ids) | Account IDs that require access to the function artifacts | `string` | `""` | no | +| [lambda\_runtime](#input\_lambda\_runtime) | Lambda runtime | `string` | `"nodejs12.x"` | no | +| [pipeline\_code\_directory](#input\_pipeline\_code\_directory) | Directory where the pipeline code is located | `string` | `"lambda-ping-sns"` | no | +| [pipeline\_packaging\_command](#input\_pipeline\_packaging\_command) | Command to run packaging | `string` | `"zip function.zip app.js"` | no | +| [pipeline\_unit\_test\_command](#input\_pipeline\_unit\_test\_command) | Command to run unit tests | `string` | `"echo 'add your unit test command here'"` | no | +| [repository\_branch\_name](#input\_repository\_branch\_name) | Branch name of the repository | `string` | `"main"` | no | +| [repository\_connection\_arn](#input\_repository\_connection\_arn) | Connection ARN for the repository | `string` | `""` | no | +| [repository\_id](#input\_repository\_id) | ID of the repository | `string` | `""` | no | +| [service\_name](#input\_service\_name) | Name of the service | `string` | `"apigw-lambda-svc"` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [pipeline\_endpoint](#output\_pipeline\_endpoint) | CodePipeline endpoint URL | + diff --git a/service-templates/apigw-lambda-svc/v1/pipeline_infrastructure/config.tf b/service-templates/apigw-lambda-svc/v1/pipeline_infrastructure/config.tf deleted file mode 100644 index 517a373..0000000 --- a/service-templates/apigw-lambda-svc/v1/pipeline_infrastructure/config.tf +++ /dev/null @@ -1,27 +0,0 @@ -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - version = "~> 3.0" - } - } - - backend "s3" {} -} - -# Configure the AWS Provider -provider "aws" { - region = var.aws_region - alias = "default" - - default_tags { - tags = { - "proton:pipeline" = var.service.name - } - } -} - -variable "aws_region" { - type = string - default = "us-east-1" -} \ No newline at end of file diff --git a/service-templates/apigw-lambda-svc/v1/pipeline_infrastructure/data.tf b/service-templates/apigw-lambda-svc/v1/pipeline_infrastructure/data.tf deleted file mode 100644 index 91fcc95..0000000 --- a/service-templates/apigw-lambda-svc/v1/pipeline_infrastructure/data.tf +++ /dev/null @@ -1,308 +0,0 @@ -data "aws_region" "current" {} - -data "aws_caller_identity" "current" {} - -data "aws_partition" "current" {} - -data "aws_iam_policy_document" "function_bucket_policy_document" { - statement { - principals { - type = "AWS" - identifiers = [for id in split(",", var.pipeline.inputs.environment_account_ids) : "arn:aws:iam::${id}:root"] - } - actions = [ - "s3:GetObject" - ] - resources = [ - aws_s3_bucket.function_bucket.arn, - "${aws_s3_bucket.function_bucket.arn}/*" - ] - } -} - -data "aws_iam_policy_document" "publish_role_policy_document" { - statement { - effect = "Allow" - resources = [ - "arn:aws:logs:${local.region}:${local.account_id}:log-group:/aws/codebuild/${aws_codebuild_project.build_project.name}", - "arn:aws:logs:${local.region}:${local.account_id}:log-group:/aws/codebuild/${aws_codebuild_project.build_project.name}*" - ] - actions = [ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents" - ] - } - statement { - effect = "Allow" - resources = [ - "arn:aws:codebuild:${local.region}:${local.account_id}:report-group:/${aws_codebuild_project.build_project.name}*", - ] - actions = [ - "codebuild:CreateReportGroup", - "codebuild:CreateReport", - "codebuild:UpdateReport", - "codebuild:BatchPutTestCases" - ] - } - statement { - effect = "Allow" - resources = ["*"] - actions = ["proton:GetService"] - } - statement { - effect = "Allow" - resources = [ - aws_s3_bucket.function_bucket.arn, - "${aws_s3_bucket.function_bucket.arn}/*" - ] - actions = [ - "s3:GetObject*", - "s3:GetBucket*", - "s3:List*", - "s3:DeleteObject*", - "s3:PutObject*", - "s3:Abort*", - "s3:CreateMultipartUpload" - ] - } - statement { - effect = "Allow" - resources = [ - aws_s3_bucket.pipeline_artifacts_bucket.arn, - "${aws_s3_bucket.pipeline_artifacts_bucket.arn}*" - ] - actions = [ - "s3:GetObject*", - "s3:GetBucket*", - "s3:List*", - "s3:DeleteObject*", - "s3:PutObject*", - "s3:Abort*" - ] - } - statement { - effect = "Allow" - resources = [aws_kms_key.pipeline_artifacts_bucket_key.arn] - actions = [ - "kms:Decrypt", - "kms:DescribeKey", - "kms:Encrypt", - "kms:ReEncrypt*", - "kms:GenerateDataKey*" - ] - } -} - -data "aws_iam_policy_document" "deployment_role_policy" { - statement { - effect = "Allow" - resources = [ - "arn:aws:logs:${local.region}:${local.account_id}:log-group:/aws/codebuild/Deploy*Project*", - "arn:aws:logs:${local.region}:${local.account_id}:log-group:/aws/codebuild/Deploy*Project:*", - ] - actions = [ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents" - ] - } - statement { - effect = "Allow" - resources = [ - "arn:aws:codebuild:${local.region}:${local.account_id}:report-group:/Deploy*Project-*", - ] - actions = [ - "codebuild:CreateReportGroup", - "codebuild:CreateReport", - "codebuild:UpdateReport", - "codebuild:BatchPutTestCases" - ] - } - statement { - effect = "Allow" - resources = ["*"] - actions = [ - "proton:GetServiceInstance", - "proton:UpdateServiceInstance" - ] - } - statement { - effect = "Allow" - resources = [ - aws_s3_bucket.pipeline_artifacts_bucket.arn, - "${aws_s3_bucket.pipeline_artifacts_bucket.arn}/*" - ] - actions = [ - "s3:GetObject*", - "s3:GetBucket*", - "s3:List*" - ] - } - statement { - effect = "Allow" - resources = [aws_kms_key.pipeline_artifacts_bucket_key.arn] - actions = [ - "kms:Decrypt", - "kms:DescribeKey", - "kms:Encrypt", - "kms:ReEncrypt*", - "kms:GenerateDataKey*" - ] - } -} - -data "aws_iam_policy_document" "pipeline_artifacts_bucket_key_policy" { - statement { - effect = "Allow" - resources = ["*"] - principals { - identifiers = ["arn:aws:iam::${local.account_id}:root"] - type = "AWS" - } - actions = [ - "kms:Create*", - "kms:Describe*", - "kms:Enable*", - "kms:List*", - "kms:Put*", - "kms:Update*", - "kms:Revoke*", - "kms:Disable*", - "kms:Get*", - "kms:Delete*", - "kms:ScheduleKeyDeletion", - "kms:CancelKeyDeletion", - "kms:GenerateDataKey", - "kms:TagResource", - "kms:UntagResource" - ] - } - - statement { - effect = "Allow" - resources = ["*"] - principals { - identifiers = [aws_iam_role.pipeline_role.arn] - type = "AWS" - } - actions = [ - "kms:Decrypt", - "kms:DescribeKey", - "kms:Encrypt", - "kms:ReEncrypt*", - "kms:GenerateDataKey*" - ] - } - - statement { - effect = "Allow" - resources = ["*"] - principals { - identifiers = [aws_iam_role.publish_role.arn] - type = "AWS" - } - actions = [ - "kms:Decrypt", - "kms:DescribeKey", - "kms:Encrypt", - "kms:ReEncrypt*", - "kms:GenerateDataKey*" - ] - } - - statement { - effect = "Allow" - resources = ["*"] - principals { - #todo - - identifiers = [aws_iam_role.deployment_role.arn] - type = "AWS" - } - actions = [ - "kms:DescribeKey", - "kms:Decrypt", - "kms:Encrypt", - "kms:ReEncrypt*", - "kms:GenerateDataKey*" - ] - } -} - -data "aws_iam_policy_document" "pipeline_role_policy" { - statement { - effect = "Allow" - resources = [ - aws_s3_bucket.pipeline_artifacts_bucket.arn, - "${aws_s3_bucket.pipeline_artifacts_bucket.arn}*" - ] - actions = [ - "s3:GetObject*", - "s3:GetBucket*", - "s3:List*", - "s3:DeleteObject*", - "s3:PutObject*", - "s3:Abort*" - ] - } - - statement { - effect = "Allow" - resources = [aws_kms_key.pipeline_artifacts_bucket_key.arn] - actions = [ - "kms:Decrypt", - "kms:DescribeKey", - "kms:Encrypt", - "kms:ReEncrypt*", - "kms:GenerateDataKey*" - ] - } - - statement { - effect = "Allow" - resources = ["*"] - actions = [ - "codestar-connections:*" - ] - } - - statement { - effect = "Allow" - resources = [aws_iam_role.pipeline_build_codepipeline_action_role.arn] - actions = [ - "sts:AssumeRole" - ] - } - - statement { - effect = "Allow" - resources = [aws_iam_role.pipeline_deploy_codepipeline_action_role.arn] - actions = [ - "sts:AssumeRole" - ] - } -} - -data "aws_iam_policy_document" "pipeline_build_codepipeline_action_role_policy" { - statement { - effect = "Allow" - resources = [aws_codebuild_project.build_project.arn] - actions = [ - "codebuild:BatchGetBuilds", - "codebuild:StartBuild", - "codebuild:StopBuild" - ] - } -} - -data "aws_iam_policy_document" "pipeline_deploy_codepipeline_action_role_policy" { - statement { - effect = "Allow" - resources = ["arn:aws:codebuild:${local.region}:${local.account_id}:project/Deploy*", ] - actions = [ - "codebuild:BatchGetBuilds", - "codebuild:StartBuild", - "codebuild:StopBuild" - ] - } -} \ No newline at end of file diff --git a/service-templates/apigw-lambda-svc/v1/pipeline_infrastructure/example.terraform.tfvars b/service-templates/apigw-lambda-svc/v1/pipeline_infrastructure/example.terraform.tfvars new file mode 100644 index 0000000..d20de1a --- /dev/null +++ b/service-templates/apigw-lambda-svc/v1/pipeline_infrastructure/example.terraform.tfvars @@ -0,0 +1,12 @@ +# Copy+paste and rename to `terraform.tfvars` for working locally +service_name = "terraform-test-service" + +codebuild_deployments = { + foo = {} + two = { + name = "bar" + } +} + +repository_connection_arn = "arn:aws:codecommit:us-east-1:123456789012:my-repository" +repository_id = "my-repository" diff --git a/service-templates/apigw-lambda-svc/v1/pipeline_infrastructure/locals.tf b/service-templates/apigw-lambda-svc/v1/pipeline_infrastructure/locals.tf deleted file mode 100644 index 2714438..0000000 --- a/service-templates/apigw-lambda-svc/v1/pipeline_infrastructure/locals.tf +++ /dev/null @@ -1,4 +0,0 @@ -locals { - account_id = data.aws_caller_identity.current.account_id - region = data.aws_region.current.id -} \ No newline at end of file diff --git a/service-templates/apigw-lambda-svc/v1/pipeline_infrastructure/main.tf b/service-templates/apigw-lambda-svc/v1/pipeline_infrastructure/main.tf index fa5b97e..4b2f2fd 100644 --- a/service-templates/apigw-lambda-svc/v1/pipeline_infrastructure/main.tf +++ b/service-templates/apigw-lambda-svc/v1/pipeline_infrastructure/main.tf @@ -1,393 +1,38 @@ -resource "aws_s3_bucket" "function_bucket" { - bucket_prefix = "function-bucket" -} +################################################################################ +# Terraform Backend & Provider +################################################################################ -resource "aws_s3_bucket_server_side_encryption_configuration" "aes256" { - bucket = aws_s3_bucket.function_bucket.id +terraform { + required_version = ">= 0.13.1" - rule { - apply_server_side_encryption_by_default { - sse_algorithm = "AES256" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 4.0" } } -} -resource "aws_s3_bucket_policy" "function_bucket_policy" { - count = var.pipeline.inputs.environment_account_ids != "" ? 1 : 0 - policy = data.aws_iam_policy_document.function_bucket_policy_document.json - bucket = aws_s3_bucket.function_bucket.id + backend "s3" {} } -resource "aws_codebuild_project" "build_project" { - name = "${var.service.name}-build-project" - service_role = aws_iam_role.publish_role.arn - - artifacts { - type = "CODEPIPELINE" - } - - environment { - compute_type = "BUILD_GENERAL1_SMALL" - image = "aws/codebuild/amazonlinux2-x86_64-standard:3.0" - type = "LINUX_CONTAINER" - image_pull_credentials_type = "CODEBUILD" - - environment_variable { - name = "bucket_name" - value = aws_s3_bucket.function_bucket.bucket - } +provider "aws" { + region = var.aws_region - environment_variable { - name = "service_name" - value = var.service.name + default_tags { + tags = { + "proton:pipeline" = var.service_name } } - - source { - buildspec = < yq_linux_amd64.sha", - "wget https://github.com/mikefarah/yq/releases/download/3.4.0/yq_linux_amd64", - "sha256sum -c yq_linux_amd64.sha", - "mv yq_linux_amd64 /usr/bin/yq", - "chmod +x /usr/bin/yq" - ] - }, - "pre_build": { - "commands": [ - "cd $CODEBUILD_SRC_DIR/${var.pipeline.inputs.code_dir}", - "${var.pipeline.inputs.unit_test_command}" - ] - }, - "build": { - "commands": [ - "${var.pipeline.inputs.packaging_command}", - "FUNCTION_KEY=$CODEBUILD_BUILD_NUMBER/function.zip", - "aws s3 cp function.zip s3://$bucket_name/$FUNCTION_KEY" - ] - }, - "post_build": { - "commands": [ - "aws proton --region $AWS_DEFAULT_REGION get-service --name $service_name | jq -r .service.spec > service.yaml", - "yq w service.yaml 'instances[*].spec.lambda_bucket' \"$bucket_name\" > rendered_service.yaml", - "yq w service.yaml 'instances[*].spec.lambda_key' \"$FUNCTION_KEY\" > rendered_service.yaml" - ] - } - }, - "artifacts": { - "files": [ - "${var.pipeline.inputs.code_dir}/rendered_service.yaml" - ] - } - } -EOF - -type = "CODEPIPELINE" } -encryption_key = aws_kms_key.pipeline_artifacts_bucket_key.arn -} - - -resource "aws_codebuild_project" "deploy_project" { - for_each = { for instance in var.service_instances : instance.name => instance } - - name = "Deploy${index(var.service_instances, each.value)}Project" - service_role = aws_iam_role.deployment_role.arn - - artifacts { - type = "CODEPIPELINE" - } - - environment { - compute_type = "BUILD_GENERAL1_SMALL" - image = "aws/codebuild/amazonlinux2-x86_64-standard:3.0" - type = "LINUX_CONTAINER" - privileged_mode = false - image_pull_credentials_type = "CODEBUILD" - - environment_variable { - name = "service_instance_name" - value = each.value.name - } - - environment_variable { - name = "service_name" - value = var.service.name - } - } - - source { - type = "CODEPIPELINE" - buildspec = < 0 ? 1 : 0 + + policy = data.aws_iam_policy_document.function.json + bucket = aws_s3_bucket.function.id +} + +data "aws_iam_policy_document" "function" { + statement { + principals { + type = "AWS" + identifiers = [for id in local.environment_account_ids : "arn:aws:iam::${id}:root"] + } + actions = [ + "s3:GetObject" + ] + } +} + +################################################################################ +# S3 Bucket - Pipeline Artifacts +################################################################################ + +resource "aws_s3_bucket" "pipeline_artifacts" { + bucket_prefix = "pipeline-artifacts-bucket" +} + +resource "aws_s3_bucket_public_access_block" "pipeline_artifacts" { + bucket = aws_s3_bucket.pipeline_artifacts.id + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "pipeline_artifacts" { + bucket = aws_s3_bucket.pipeline_artifacts.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "aws:kms" + kms_master_key_id = aws_kms_key.pipeline_artifacts.arn + } + } +} + +################################################################################ +# KMS Key - Pipeline Artifact +################################################################################ + +resource "aws_kms_key" "pipeline_artifacts" { + policy = data.aws_iam_policy_document.pipeline_artifacts_kms.json +} + +resource "aws_kms_alias" "pipeline_artifacts" { + target_key_id = aws_kms_key.pipeline_artifacts.id + name = "alias/codepipeline-encryption-key-${var.service_name}" +} + +data "aws_iam_policy_document" "pipeline_artifacts_kms" { + statement { + sid = "KeyAdmin" + + actions = [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource" + ] + + resources = ["*"] + + principals { + identifiers = ["arn:aws:iam::${local.account_id}:root"] + type = "AWS" + } + } + + statement { + sid = "KeyUsage" + + actions = [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ] + + resources = ["*"] + + principals { + identifiers = [ + aws_iam_role.build.arn, + aws_iam_role.deploy.arn, + aws_iam_role.pipeline.arn + ] + type = "AWS" + } + } +} + +################################################################################ +# IAM Role - Build +################################################################################ + +resource "aws_iam_role" "build" { + name_prefix = "build-role" + + assume_role_policy = jsonencode({ + "Version" : "2012-10-17", + "Statement" : [ + { + "Effect" : "Allow", + "Principal" : { + "Service" : "codebuild.amazonaws.com" + }, + "Action" : "sts:AssumeRole" + } + ] + }) +} + +data "aws_iam_policy_document" "build" { + statement { + effect = "Allow" + resources = [ + "arn:aws:logs:${local.region}:${local.account_id}:log-group:/aws/codebuild/${aws_codebuild_project.build.name}", + "arn:aws:logs:${local.region}:${local.account_id}:log-group:/aws/codebuild/${aws_codebuild_project.build.name}*" + ] + actions = [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ] + } + statement { + effect = "Allow" + resources = [ + "arn:aws:codebuild:${local.region}:${local.account_id}:report-group:/${aws_codebuild_project.build.name}*", + ] + actions = [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases" + ] + } + statement { + effect = "Allow" + resources = ["*"] + actions = ["proton:GetService"] + } + statement { + effect = "Allow" + resources = [ + aws_s3_bucket.function.arn, + "${aws_s3_bucket.function.arn}/*" + ] + actions = [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*", + "s3:CreateMultipartUpload" + ] + } + statement { + effect = "Allow" + resources = [ + aws_s3_bucket.pipeline_artifacts.arn, + "${aws_s3_bucket.pipeline_artifacts.arn}*" + ] + actions = [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*" + ] + } + statement { + effect = "Allow" + resources = [aws_kms_key.pipeline_artifacts.arn] + actions = [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ] + } +} + +resource "aws_iam_policy" "build" { + policy = data.aws_iam_policy_document.build.json +} + +resource "aws_iam_role_policy_attachment" "build" { + policy_arn = aws_iam_policy.build.arn + role = aws_iam_role.build.name +} + +################################################################################ +# CodeBuild Project - Build +################################################################################ + +locals { + # Lookup map that converts shorthand syntax to expanded form CodePipeline accepts + buildspec_runtime = { + "ruby2.7" = { + "ruby" = "2.7" + }, + "nodejs12.x" = { + "nodejs" = "12.x" + }, + "python3.8" = { + "python" = "3.8" + }, + "java11" = { + "java" = "openjdk11.x" + }, + "dotnetcore3.1" = { + "dotnet" = "3.1" + } + } +} + +resource "aws_codebuild_project" "build" { + name = "${var.service_name}-build-project" + service_role = aws_iam_role.build.arn + + encryption_key = aws_kms_key.pipeline_artifacts.arn + + artifacts { + type = "CODEPIPELINE" + } + + environment { + compute_type = "BUILD_GENERAL1_SMALL" + image = "aws/codebuild/amazonlinux2-x86_64-standard:3.0" + type = "LINUX_CONTAINER" + image_pull_credentials_type = "CODEBUILD" + + environment_variable { + name = "bucket_name" + value = aws_s3_bucket.function.bucket + } + + environment_variable { + name = "service_name" + value = var.service_name + } + } + + source { + type = "CODEPIPELINE" + buildspec = <<-EOF + { + "version": "0.2", + "phases": { + "install": { + "runtime-versions": { + ${jsonencode(local.buildspec_runtime[var.lambda_runtime])} + }, + "commands": [ + "pip3 install --upgrade --user awscli", + "echo 'f6bd1536a743ab170b35c94ed4c7c4479763356bd543af5d391122f4af852460 yq_linux_amd64' > yq_linux_amd64.sha", + "wget https://github.com/mikefarah/yq/releases/download/3.4.0/yq_linux_amd64", + "sha256sum -c yq_linux_amd64.sha", + "mv yq_linux_amd64 /usr/bin/yq", + "chmod +x /usr/bin/yq" + ] + }, + "pre_build": { + "commands": [ + "cd $CODEBUILD_SRC_DIR/${var.pipeline_code_directory}", + "${var.pipeline_unit_test_command}" + ] + }, + "build": { + "commands": [ + "${var.pipeline_packaging_command}", + "FUNCTION_KEY=$CODEBUILD_BUILD_NUMBER/function.zip", + "aws s3 cp function.zip s3://$bucket_name/$FUNCTION_KEY" + ] + }, + "post_build": { + "commands": [ + "aws proton --region $AWS_DEFAULT_REGION get-service --name $service_name | jq -r .service.spec > service.yaml", + "yq w service.yaml 'instances[*].spec.lambda_bucket' \"$bucket_name\" > rendered_service.yaml", + "yq w service.yaml 'instances[*].spec.lambda_key' \"$FUNCTION_KEY\" > rendered_service.yaml" + ] + } + }, + "artifacts": { + "files": [ + "${var.pipeline_code_directory}/rendered_service.yaml" + ] + } + } + EOF + } +} + +################################################################################ +# IAM Role - Deploy +################################################################################ + +resource "aws_iam_role" "deploy" { + name_prefix = "deployment-role" + + assume_role_policy = jsonencode({ + "Version" : "2012-10-17", + "Statement" : [ + { + "Effect" : "Allow", + "Principal" : { + "Service" : "codebuild.amazonaws.com" + }, + "Action" : "sts:AssumeRole" + } + ] + }) +} + +data "aws_iam_policy_document" "deploy" { + statement { + actions = [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ] + + resources = [ + "arn:aws:logs:${local.region}:${local.account_id}:log-group:/aws/codebuild/Deploy*Project*", + "arn:aws:logs:${local.region}:${local.account_id}:log-group:/aws/codebuild/Deploy*Project:*", + ] + } + + statement { + actions = [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases" + ] + + resources = [ + "arn:aws:codebuild:${local.region}:${local.account_id}:report-group:/Deploy*Project-*", + ] + } + + statement { + actions = [ + "proton:GetServiceInstance", + "proton:UpdateServiceInstance" + ] + + resources = ["*"] + } + + statement { + actions = [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ] + + resources = [ + aws_s3_bucket.pipeline_artifacts.arn, + "${aws_s3_bucket.pipeline_artifacts.arn}/*" + ] + } + + statement { + actions = [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ] + + resources = [aws_kms_key.pipeline_artifacts.arn] + } +} + +resource "aws_iam_policy" "deploy" { + policy = data.aws_iam_policy_document.deploy.json +} + +resource "aws_iam_role_policy_attachment" "deploy" { + policy_arn = aws_iam_policy.deploy.arn + role = aws_iam_role.deploy.name +} + +################################################################################ +# CodeBuild Project - Deploy +################################################################################ + +resource "aws_codebuild_project" "deploy" { + for_each = var.codebuild_deployments + + name = "Deploy${try(each.value.name, each.key)}Project" + service_role = aws_iam_role.deploy.arn + + artifacts { + type = "CODEPIPELINE" + } + + environment { + compute_type = "BUILD_GENERAL1_SMALL" + image = "aws/codebuild/amazonlinux2-x86_64-standard:3.0" + type = "LINUX_CONTAINER" + privileged_mode = false + image_pull_credentials_type = "CODEBUILD" + + environment_variable { + name = "service_instance_name" + value = try(each.value.name, each.key) + } + + environment_variable { + name = "service_name" + value = var.service_name + } + } + + source { + type = "CODEPIPELINE" + buildspec = <<-EOF + { + "version": "0.2", + "phases": { + "build": { + "commands": [ + "pip3 install --upgrade --user awscli", + "aws proton --region $AWS_DEFAULT_REGION update-service-instance --deployment-type CURRENT_VERSION --name $service_instance_name --service-name $service_name --spec file://${var.pipeline_code_directory}/rendered_service.yaml", + "aws proton --region $AWS_DEFAULT_REGION wait service-instance-deployed --name $service_instance_name --service-name $service_name" + ] + } + } + } + EOF + } + + encryption_key = aws_kms_key.pipeline_artifacts.arn +} + +################################################################################ +# IAM Role - CodePipeline +################################################################################ + +resource "aws_iam_role" "pipeline" { + name_prefix = "pipeline-role" + + assume_role_policy = jsonencode({ + "Version" : "2012-10-17", + "Statement" : [ + { + "Effect" : "Allow", + "Principal" : { + "Service" : "codepipeline.amazonaws.com" + }, + "Action" : "sts:AssumeRole" + } + ] + }) +} + +data "aws_iam_policy_document" "pipeline" { + statement { + actions = [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*" + ] + + resources = [ + aws_s3_bucket.pipeline_artifacts.arn, + "${aws_s3_bucket.pipeline_artifacts.arn}*" + ] + } + + statement { + actions = [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ] + + resources = [aws_kms_key.pipeline_artifacts.arn] + } + + statement { + actions = ["codestar-connections:*"] + resources = ["*"] + } + + statement { + actions = ["sts:AssumeRole"] + resources = [ + aws_iam_role.build.arn, + aws_iam_role.deploy.arn + ] + } +} + +resource "aws_iam_policy" "pipeline" { + policy = data.aws_iam_policy_document.pipeline.json +} + +resource "aws_iam_role_policy_attachment" "pipeline" { + policy_arn = aws_iam_policy.pipeline.arn + role = aws_iam_role.pipeline.name +} + +################################################################################ +# IAM Role - CodePipeline Action +################################################################################ + +resource "aws_iam_role" "pipeline_action" { + name_prefix = "pipeline-deploy-action-role" + + assume_role_policy = jsonencode({ + "Version" : "2012-10-17", + "Statement" : [ + { + "Effect" : "Allow", + "Principal" : { + "AWS" : "arn:aws:iam::${local.account_id}:root" + }, + "Action" : "sts:AssumeRole" + } + ] + }) +} + +data "aws_iam_policy_document" "pipeline_action" { + statement { + actions = [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ] + + resources = ["arn:aws:codebuild:${local.region}:${local.account_id}:project/Deploy*"] + } +} + +resource "aws_iam_policy" "pipeline_action" { + policy = data.aws_iam_policy_document.pipeline_action.json +} + +resource "aws_iam_role_policy_attachment" "pipeline_action" { + policy_arn = aws_iam_policy.pipeline_action.arn + role = aws_iam_role.pipeline_action.name +} + +################################################################################ +# CodePipeline +################################################################################ + +resource "aws_codepipeline" "pipeline" { + name = "${var.service_name}-pipeline" + role_arn = aws_iam_role.pipeline.arn + + stage { + name = "Source" + + action { + category = "Source" + name = "Checkout" + owner = "AWS" + provider = "CodeStarSourceConnection" + version = "1" + run_order = 1 + + configuration = { + ConnectionArn : var.repository_connection_arn + FullRepositoryId : var.repository_id + BranchName : var.repository_branch_name + } + + output_artifacts = ["Artifact_Source_Checkout"] + } + } + + stage { + name = "Build" + + action { + category = "Build" + name = "Build" + owner = "AWS" + provider = "CodeBuild" + version = "1" + run_order = 1 + + configuration = { + ProjectName = aws_codebuild_project.build.name + } + + input_artifacts = ["Artifact_Source_Checkout"] + output_artifacts = ["BuildOutput"] + role_arn = aws_iam_role.pipeline_action.arn + } + } + + dynamic "stage" { + for_each = var.codebuild_deployments + + content { + name = "Deploy${try(each.value.name, each.key)}Project" + + action { + category = "Build" + name = "Deploy${try(each.value.name, each.key)}" + owner = "AWS" + provider = "CodeBuild" + version = "1" + run_order = 1 + + configuration = { + ProjectName = "Deploy${try(each.value.name, each.key)}Project" + } + + input_artifacts = ["BuildOutput"] + role_arn = aws_iam_role.pipeline_action.arn + } + } + } + + artifact_store { + encryption_key { + id = aws_kms_key.pipeline_artifacts.arn + type = "KMS" + } + + location = aws_s3_bucket.pipeline_artifacts.bucket + type = "S3" + } +} diff --git a/service-templates/apigw-lambda-svc/v1/pipeline_infrastructure/variables.tf b/service-templates/apigw-lambda-svc/v1/pipeline_infrastructure/variables.tf new file mode 100644 index 0000000..744b6d2 --- /dev/null +++ b/service-templates/apigw-lambda-svc/v1/pipeline_infrastructure/variables.tf @@ -0,0 +1,65 @@ +variable "aws_region" { + description = "Region where resources will be provisioned" + type = string + default = "us-east-1" +} + +variable "service_name" { + description = "Name of the service" + type = string + default = "apigw-lambda-svc" +} + +variable "lambda_runtime" { + description = "Lambda runtime" + type = string + default = "nodejs12.x" +} + +variable "environment_account_ids" { + description = "Account IDs that require access to the function artifacts" + type = string + default = "" +} + +variable "codebuild_deployments" { + description = "CodeBuild deployment projects" + type = any + default = {} +} + +variable "pipeline_code_directory" { + description = "Directory where the pipeline code is located" + type = string + default = "lambda-ping-sns" +} + +variable "pipeline_unit_test_command" { + description = "Command to run unit tests" + type = string + default = "echo 'add your unit test command here'" +} + +variable "pipeline_packaging_command" { + description = "Command to run packaging" + type = string + default = "zip function.zip app.js" +} + +variable "repository_connection_arn" { + description = "Connection ARN for the repository" + type = string + default = "" +} + +variable "repository_id" { + description = "ID of the repository" + type = string + default = "" +} + +variable "repository_branch_name" { + description = "Branch name of the repository" + type = string + default = "main" +} diff --git a/service-templates/apigw-lambda-svc/v1/schema/schema.yaml b/service-templates/apigw-lambda-svc/v1/schema/schema.yaml index b3135cb..a7cdc62 100644 --- a/service-templates/apigw-lambda-svc/v1/schema/schema.yaml +++ b/service-templates/apigw-lambda-svc/v1/schema/schema.yaml @@ -9,62 +9,51 @@ schema: type: object description: "Input properties for a Lambda backed HTTP API." properties: + lambda_function_name: + type: string + description: "The name to be assigned to the Lambda function" + minLength: 1 + maxLength: 50 + default: "function" lambda_handler: type: string description: "The function within your code that is called to begin execution" minLength: 1 maxLength: 50 default: "app.handler" - lambda_memory: - type: number - description: "The size of your Lambda functions in MB" - default: 512 - minimum: 1 - maximum: 3008 - lambda_timeout: - type: number - description: "The timeout in seconds of your Lambda function" - default: 30 - minimum: 1 - maximum: 900 lambda_runtime: type: string description: "The runtime for your Lambda service" enum: ["nodejs12.x", "python3.8", "ruby2.7", "java11", "go1.x", "dotnetcore3.1"] default: "nodejs12.x" - lambda_bucket: + lambda_s3_bucket: type: string description: "The s3 bucket for your application code" minLength: 1 maxLength: 200 - lambda_key: + lambda_s3_key: type: string description: "The s3 key for your application code" minLength: 1 maxLength: 200 - subnet_type: - type: string - description: "Subnet type for your service" - enum: ["public", "private"] - default: "public" PipelineInputs: type: object description: "Pipeline input properties" properties: - code_dir: + pipeline_code_directory: type: string description: "Source directory for the service" default: "lambda-ping-sns" minLength: 1 maxLength: 100 - unit_test_command: + pipeline_unit_test_command: type: string description: "The command to run to unit test the application code" default: "echo 'add your unit test command here'" minLength: 1 maxLength: 200 - packaging_command: + pipeline_packaging_command: type: string description: "The commands which packages your code into a file called function.zip" default: "zip function.zip app.js"