From 83f50be32e8c86c3efb83ac86a4209795b677b68 Mon Sep 17 00:00:00 2001 From: Anton Babenko Date: Wed, 10 Jun 2020 13:41:11 +0200 Subject: [PATCH] feat: Added deploy module to do complex deployments using AWS CodeDeploy (#17) --- README.md | 38 ++++-- examples/deploy/.gitignore | 2 + examples/deploy/README.md | 46 +++++++ examples/deploy/main.tf | 91 ++++++++++++++ examples/deploy/outputs.tf | 34 ++++++ modules/deploy/README.md | 172 ++++++++++++++++++++++++++ modules/deploy/main.tf | 235 ++++++++++++++++++++++++++++++++++++ modules/deploy/outputs.tf | 34 ++++++ modules/deploy/variables.tf | 195 ++++++++++++++++++++++++++++++ modules/deploy/versions.tf | 7 ++ 10 files changed, 843 insertions(+), 11 deletions(-) create mode 100644 examples/deploy/.gitignore create mode 100644 examples/deploy/README.md create mode 100644 examples/deploy/main.tf create mode 100644 examples/deploy/outputs.tf create mode 100644 modules/deploy/README.md create mode 100644 modules/deploy/main.tf create mode 100644 modules/deploy/outputs.tf create mode 100644 modules/deploy/variables.tf create mode 100644 modules/deploy/versions.tf diff --git a/README.md b/README.md index 7a4106c3..c73e13b5 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,11 @@ Not supported, yet: This Terraform module is the part of [serverless.tf framework](https://github.com/antonbabenko/serverless.tf), which aims to simplify all operations when working with the serverless in Terraform: -1. Build and install dependencies - [read more](#build) -2. Create, store, and use deployment packages - [read more](#package) -3. Create and update AWS Lambda Function and Lambda Layer - [see usage](#usage) -4. Publish and create aliases for AWS Lambda Function - [see usage](#usage) -5. Do complex deployments (eg, rolling, canary, rollbacks) - [read more](#deployment) +1. Build and install dependencies - [read more](#build). +2. Create, store, and use deployment packages - [read more](#package). +3. Create, update, and publish AWS Lambda Function and Lambda Layer - [see usage](#usage). +4. Create static and dynamic aliases for AWS Lambda Function - [see usage](#usage), see [modules/alias](https://github.com/terraform-aws-modules/terraform-aws-lambda/tree/master/modules/alias). +5. Do complex deployments (eg, rolling, canary, rollbacks, triggers) - [read more](#deployment), see [modules/deploy](https://github.com/terraform-aws-modules/terraform-aws-lambda/tree/master/modules/deploy). ## Features @@ -124,7 +124,19 @@ module "lambda_function_existing_package_local" { ### Lambda Function with existing package (prebuilt) stored in S3 bucket +Note that this module does not copy prebuilt packages into S3 bucket. This module can only store packages it builds locally and in S3 bucket. + ```hcl +locals { + my_function_source = "../path/to/package.zip" +} + +resource "aws_s3_bucket_object" "my_function" { + bucket = "my-bucket-with-lambda-builds" + key = "${filemd5(local.my_function_source)}.zip" + source = local.my_function_source +} + module "lambda_function_existing_package_s3" { source = "terraform-aws-modules/lambda/aws" @@ -136,7 +148,7 @@ module "lambda_function_existing_package_s3" { create_package = false s3_existing_package = { bucket = "my-bucket-with-lambda-builds" - key = "existing_package.zip" + key = aws_s3_bucket_object.my_function.id } } ``` @@ -466,7 +478,9 @@ In simple terms, Lambda alias is like a pointer to either one version of Lambda One Lambda Function can be used in multiple aliases. Using aliases gives large control of which version deployed when having multiple environments. -There is [alias module](https://github.com/terraform-aws-modules/terraform-aws-lambda/tree/master/modules/alias), which simplifies working with alias (create, manage configurations, updates, etc). +There is [alias module](https://github.com/terraform-aws-modules/terraform-aws-lambda/tree/master/modules/alias), which simplifies working with alias (create, manage configurations, updates, etc). See [examples/alias](https://github.com/terraform-aws-modules/terraform-aws-lambda/tree/master/examples/alias) for various use-cases how aliases can be configured and used. + +There is [deploy module](https://github.com/terraform-aws-modules/terraform-aws-lambda/tree/master/modules/deploy), which creates required resources to do deployments using AWS CodeDeploy. It also creates the deployment, and wait for completion. See [examples/deploy](https://github.com/terraform-aws-modules/terraform-aws-lambda/tree/master/examples/deploy) for complete end-to-end build/update/deploy process. ## FAQ @@ -488,10 +502,12 @@ A2: Delete an existing zip-archive from `builds` directory, or make a change in ## Examples -* [Complete](https://github.com/terraform-aws-modules/terraform-aws-lambda/tree/master/examples/complete) - Create Lambda resources in various combinations with all supported features -* [Build and Package](https://github.com/terraform-aws-modules/terraform-aws-lambda/tree/master/examples/build-package) - Build and create deployment packages in various ways -* [Async Invocations](https://github.com/terraform-aws-modules/terraform-aws-lambda/tree/master/examples/async) - Create Lambda Function with async event configuration (with SQS and SNS integration) -* [With VPC](https://github.com/terraform-aws-modules/terraform-aws-lambda/tree/master/examples/with-vpc) - Create Lambda Function with VPC +* [Complete](https://github.com/terraform-aws-modules/terraform-aws-lambda/tree/master/examples/complete) - Create Lambda resources in various combinations with all supported features. +* [Build and Package](https://github.com/terraform-aws-modules/terraform-aws-lambda/tree/master/examples/build-package) - Build and create deployment packages in various ways. +* [Alias](https://github.com/terraform-aws-modules/terraform-aws-lambda/tree/master/examples/alias) - Create static and dynamic aliases in various ways. +* [Deploy](https://github.com/terraform-aws-modules/terraform-aws-lambda/tree/master/examples/deploy) - Complete end-to-end build/update/deploy process using AWS CodeDeploy. +* [Async Invocations](https://github.com/terraform-aws-modules/terraform-aws-lambda/tree/master/examples/async) - Create Lambda Function with async event configuration (with SQS and SNS integration). +* [With VPC](https://github.com/terraform-aws-modules/terraform-aws-lambda/tree/master/examples/with-vpc) - Create Lambda Function with VPC. diff --git a/examples/deploy/.gitignore b/examples/deploy/.gitignore new file mode 100644 index 00000000..b24c9952 --- /dev/null +++ b/examples/deploy/.gitignore @@ -0,0 +1,2 @@ +builds/* +deploy_script_*.txt diff --git a/examples/deploy/README.md b/examples/deploy/README.md new file mode 100644 index 00000000..0a7e3462 --- /dev/null +++ b/examples/deploy/README.md @@ -0,0 +1,46 @@ +# Lambda Function Deployments using AWS CodeDeploy + +Configuration in this directory creates Lambda Function, Alias, and all resources required to create deployments using AWS CodeDeploy, and then it does a real deployment. + +## Usage + +To run this example you need to execute: + +```bash +$ terraform init +$ terraform plan +$ terraform apply +``` + +Note that this example may create resources which cost money. Run `terraform destroy` when you don't need these resources. + + +## Requirements + +No requirements. + +## Providers + +| Name | Version | +|------|---------| +| aws | n/a | +| random | n/a | + +## Inputs + +No input. + +## Outputs + +| Name | Description | +|------|-------------| +| appspec | n/a | +| appspec\_content | n/a | +| appspec\_sha256 | n/a | +| codedeploy\_iam\_role\_name | Name of IAM role used by CodeDeploy | +| deploy\_script | n/a | +| script | n/a | +| this\_codedeploy\_app\_name | Name of CodeDeploy application | +| this\_codedeploy\_deployment\_group\_id | CodeDeploy deployment group name | + + diff --git a/examples/deploy/main.tf b/examples/deploy/main.tf new file mode 100644 index 00000000..c236b272 --- /dev/null +++ b/examples/deploy/main.tf @@ -0,0 +1,91 @@ +provider "aws" { + region = "eu-west-1" + + # Make it faster by skipping something + skip_get_ec2_platforms = true + skip_metadata_api_check = true + skip_region_validation = true + skip_credentials_validation = true + skip_requesting_account_id = true +} + +resource "random_pet" "this" { + length = 2 +} + +module "lambda_function" { + source = "../../" + + function_name = "${random_pet.this.id}-lambda" + handler = "index.lambda_handler" + runtime = "python3.8" + publish = true + + source_path = "${path.module}/../fixtures/python3.8-app1" + hash_extra = "yo1" + + allowed_triggers = { + APIGatewayAny = { + service = "apigateway" + arn = "arn:aws:execute-api:eu-west-1:135367859851:aqnku8akd0" + } + } +} + +module "alias_refresh" { + source = "../../modules/alias" + + refresh_alias = true + + name = "current-with-refresh" + + function_name = module.lambda_function.this_lambda_function_name + + # Set function_version when creating alias to be able to deploy using it, + # because AWS CodeDeploy doesn't understand $LATEST as CurrentVersion. + function_version = module.lambda_function.this_lambda_function_version +} + +module "deploy" { + source = "../../modules/deploy" + + alias_name = module.alias_refresh.this_lambda_alias_name + function_name = module.lambda_function.this_lambda_function_name + + target_version = module.lambda_function.this_lambda_function_version + description = "This is my awesome deploy!" + + create_app = true + app_name = "my-awesome-app" + + create_deployment_group = true + deployment_group_name = "something" + + create_deployment = true + save_deploy_script = true + wait_deployment_completion = true + force_deploy = true + + attach_triggers_policy = true + triggers = { + start = { + events = ["DeploymentStart"] + name = "DeploymentStart" + target_arn = aws_sns_topic.sns1.arn + } + success = { + events = ["DeploymentSuccess"] + name = "DeploymentSuccess" + target_arn = aws_sns_topic.sns2.arn + } + } + +} + +resource "aws_sns_topic" "sns1" { + name_prefix = random_pet.this.id +} + +resource "aws_sns_topic" "sns2" { + name_prefix = random_pet.this.id +} diff --git a/examples/deploy/outputs.tf b/examples/deploy/outputs.tf new file mode 100644 index 00000000..0bcd2597 --- /dev/null +++ b/examples/deploy/outputs.tf @@ -0,0 +1,34 @@ +output "this_codedeploy_app_name" { + description = "Name of CodeDeploy application" + value = module.deploy.this_codedeploy_app_name +} + +output "this_codedeploy_deployment_group_id" { + description = "CodeDeploy deployment group name" + value = module.deploy.this_codedeploy_deployment_group_id +} + +output "codedeploy_iam_role_name" { + description = "Name of IAM role used by CodeDeploy" + value = module.deploy.codedeploy_iam_role_name +} + +output "appspec" { + value = module.deploy.appspec +} + +output "appspec_content" { + value = module.deploy.appspec_content +} + +output "appspec_sha256" { + value = module.deploy.appspec_sha256 +} + +output "script" { + value = module.deploy.script +} + +output "deploy_script" { + value = module.deploy.deploy_script +} diff --git a/modules/deploy/README.md b/modules/deploy/README.md new file mode 100644 index 00000000..6ec5e37a --- /dev/null +++ b/modules/deploy/README.md @@ -0,0 +1,172 @@ +# Lambda Function Deployment via AWS CodeDeploy + +Terraform module, which creates Lambda alias as well as AWS CodeDeploy resources required to deploy. + +This Terraform module is the part of [serverless.tf framework](https://github.com/antonbabenko/serverless.tf), which aims to simplify all operations when working with the serverless in Terraform. + +This module can create AWS CodeDeploy application and deployment group, if necessary. If you have several functions, you probably want to create those resources externally, and then set `use_existing_deployment_group = true`. + +During deployment this module does the following: +1. Create JSON object with required AppSpec configuration. Optionally, you can store deploy script for debug purposes by setting `save_deploy_script = true`. +1. Run [`aws deploy create-deployment` command](https://docs.aws.amazon.com/cli/latest/reference/deploy/create-deployment.html) if `create_deployment = true` was set +1. After deployment is created, it can wait for the completion if `wait_deployment_completion = true`. Be aware, that Terraform will lock the execution and it can fail if it runs for a long period of time. Set this flag for fast deployments (eg, `deployment_config_name = "CodeDeployDefault.LambdaAllAtOnce"`). + + +## Usage + +### Complete example of Lambda Function deployment via AWS CodeDeploy + +```hcl +module "lambda_function" { + source = "terraform-aws-modules/lambda/aws" + + function_name = "my-lambda1" + handler = "index.lambda_handler" + runtime = "python3.8" + + source_path = "../src/lambda-function1" +} + +module "alias_refresh" { + source = "terraform-aws-modules/lambda/aws//modules/alias" + + name = "current-with-refresh" + function_name = module.lambda_function.this_lambda_function_name + + # Set function_version when creating alias to be able to deploy using it, + # because AWS CodeDeploy doesn't understand $LATEST as CurrentVersion. + function_version = module.lambda_function.this_lambda_function_version +} + +module "deploy" { + source = "terraform-aws-modules/lambda/aws//modules/deploy" + + alias_name = module.alias_refresh.this_lambda_alias_name + function_name = module.lambda_function.this_lambda_function_name + + target_version = module.lambda_function.this_lambda_function_version + + create_app = true + app_name = "my-awesome-app" + + create_deployment_group = true + deployment_group_name = "something" + + create_deployment = true + wait_deployment_completion = true + + triggers = { + start = { + events = ["DeploymentStart"] + name = "DeploymentStart" + target_arn = "arn:aws:sns:eu-west-1:135367859851:sns1" + } + success = { + events = ["DeploymentSuccess"] + name = "DeploymentSuccess" + target_arn = "arn:aws:sns:eu-west-1:135367859851:sns2" + } + } +} +``` + +## Conditional creation + +Sometimes you need to have a way to create resources conditionally but Terraform does not allow usage of `count` inside `module` block, so the solution is to specify `create` arguments. + +```hcl +module "lambda" { + source = "terraform-aws-modules/lambda/aws//modules/deploy" + + create = false # to disable all resources + + create_app = false + use_existing_app = false + create_deployment_group = false + use_existing_deployment_group = false + + # ... omitted +} +``` + +## Examples + +* [Deploy](https://github.com/terraform-aws-modules/terraform-aws-lambda/tree/master/examples/deploy) - Creates Lambda Function, Alias, and all resources required to create deployments using AWS CodeDeploy. + + + +## Requirements + +| Name | Version | +|------|---------| +| terraform | ~> 0.12.6 | +| aws | ~> 2.46 | + +## Providers + +| Name | Version | +|------|---------| +| aws | ~> 2.46 | +| local | n/a | +| null | n/a | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| after\_allow\_traffic\_hook\_arn | ARN of Lambda function to execute after allow traffic during deployment | `string` | `""` | no | +| alarm\_enabled | Indicates whether the alarm configuration is enabled. This option is useful when you want to temporarily deactivate alarm monitoring for a deployment group without having to add the same alarms again later. | `bool` | `false` | no | +| alarm\_ignore\_poll\_alarm\_failure | Indicates whether a deployment should continue if information about the current state of alarms cannot be retrieved from CloudWatch. | `bool` | `false` | no | +| alarms | A list of alarms configured for the deployment group. A maximum of 10 alarms can be added to a deployment group. | `list(string)` | `[]` | no | +| alias\_name | Name for the alias | `string` | `""` | no | +| app\_name | Name of AWS CodeDeploy application | `string` | `""` | no | +| attach\_triggers\_policy | Whether to attach SNS policy to CodeDeploy role when triggers are defined | `bool` | `false` | no | +| auto\_rollback\_enabled | Indicates whether a defined automatic rollback configuration is currently enabled for this Deployment Group. | `bool` | `true` | no | +| auto\_rollback\_events | List of event types that trigger a rollback. Supported types are DEPLOYMENT\_FAILURE and DEPLOYMENT\_STOP\_ON\_ALARM. | `list(string)` |
[
"DEPLOYMENT_STOP_ON_ALARM"
]
| no | +| aws\_cli\_command | Command to run as AWS CLI. May include extra arguments like region and profile. | `string` | `"aws"` | no | +| before\_allow\_traffic\_hook\_arn | ARN of Lambda function to execute before allow traffic during deployment | `string` | `""` | no | +| codedeploy\_principals | List of CodeDeploy service principals to allow. The list can include global or regional endpoints. | `list(string)` |
[
"codedeploy.amazonaws.com"
]
| no | +| codedeploy\_role\_name | IAM role name to create or use by CodeDeploy | `string` | `""` | no | +| create | Controls whether resources should be created | `bool` | `true` | no | +| create\_app | Whether to create new AWS CodeDeploy app | `bool` | `false` | no | +| create\_codedeploy\_role | Whether to create new AWS CodeDeploy IAM role | `bool` | `true` | no | +| create\_deployment | Run AWS CLI command to create deployment | `bool` | `false` | no | +| create\_deployment\_group | Whether to create new AWS CodeDeploy Deployment Group | `bool` | `false` | no | +| current\_version | Current version of Lambda function version to deploy (can't be $LATEST) | `string` | `""` | no | +| deployment\_config\_name | Name of deployment config to use | `string` | `"CodeDeployDefault.LambdaAllAtOnce"` | no | +| deployment\_group\_name | Name of deployment group to use | `string` | `""` | no | +| description | Description to use for the deployment | `string` | `""` | no | +| force\_deploy | Force deployment every time (even when nothing changes) | `bool` | `false` | no | +| function\_name | The name of the Lambda function to deploy | `string` | `""` | no | +| save\_deploy\_script | Save deploy script locally | `bool` | `false` | no | +| target\_version | Target version of Lambda function version to deploy | `string` | `""` | no | +| triggers | Map of triggers which will be notified when event happens. Valid options for event types are DeploymentStart, DeploymentSuccess, DeploymentFailure, DeploymentStop, DeploymentRollback, DeploymentReady (Applies only to replacement instances in a blue/green deployment), InstanceStart, InstanceSuccess, InstanceFailure, InstanceReady. Note that not all are applicable for Lambda deployments. | `map(any)` | `{}` | no | +| use\_existing\_app | Whether to use existing AWS CodeDeploy app | `bool` | `false` | no | +| use\_existing\_deployment\_group | Whether to use existing AWS CodeDeploy Deployment Group | `bool` | `false` | no | +| wait\_deployment\_completion | Wait until deployment completes. It can take a lot of time and your terraform process may lock execution for long time. | `bool` | `false` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| appspec | n/a | +| appspec\_content | n/a | +| appspec\_sha256 | n/a | +| codedeploy\_iam\_role\_name | Name of IAM role used by CodeDeploy | +| deploy\_script | n/a | +| script | n/a | +| this\_codedeploy\_app\_name | Name of CodeDeploy application | +| this\_codedeploy\_deployment\_group\_id | CodeDeploy deployment group name | + + + +## Authors + +Module managed by [Anton Babenko](https://github.com/antonbabenko). Check out [serverless.tf](https://serverless.tf) to learn more about doing serverless with Terraform. + +Please reach out to [Betajob](https://www.betajob.com/) if you are looking for commercial support for your Terraform, AWS, or serverless project. + + +## License + +Apache 2 Licensed. See LICENSE for full details. diff --git a/modules/deploy/main.tf b/modules/deploy/main.tf new file mode 100644 index 00000000..e1c5b389 --- /dev/null +++ b/modules/deploy/main.tf @@ -0,0 +1,235 @@ +locals { + # AWS CodeDeploy can't deploy when CurrentVersion is "$LATEST" + current_version = data.aws_lambda_function.this[0].qualifier == "$LATEST" ? 1 : data.aws_lambda_function.this[0].qualifier + + app_name = element(concat(aws_codedeploy_app.this.*.name, [var.app_name]), 0) + deployment_group_name = element(concat(aws_codedeploy_deployment_group.this.*.deployment_group_name, [var.deployment_group_name]), 0) + + appspec = merge({ + version = "0.0" + Resources = [ + { + MyFunction = { + Type = "AWS::Lambda::Function" + Properties = { + Name = var.function_name + Alias = var.alias_name + CurrentVersion = var.current_version != "" ? var.current_version : local.current_version + TargetVersion : var.target_version + } + } + } + ] + }, var.before_allow_traffic_hook_arn != "" || var.after_allow_traffic_hook_arn != "" ? { + Hooks = [for k, v in zipmap(["BeforeAllowTraffic", "AfterAllowTraffic"], [ + var.before_allow_traffic_hook_arn != "" ? var.before_allow_traffic_hook_arn : null, + var.after_allow_traffic_hook_arn != "" ? var.after_allow_traffic_hook_arn : null + ]) : map(k, v)] + } : {}) + + appspec_content = replace(jsonencode(local.appspec), "\"", "\\\"") + appspec_sha256 = sha256(jsonencode(local.appspec)) + + script = <