terraform-aws-modules / terraform-aws-lambda

Terraform module, which takes care of a lot of AWS Lambda/serverless tasks (build dependencies, packages, updates, deployments) in countless combinations 🇺🇦
https://registry.terraform.io/modules/terraform-aws-modules/lambda/aws
Apache License 2.0
886 stars 656 forks source link

Module does not recognize python in Path when requirements.txt is present #452

Closed bpgould closed 1 year ago

bpgould commented 1 year ago

Description

When running terraform plan or apply I get the following error:

 Traceback (most recent call last):
│   File "C:\Users\z004rc1n\Documents\WORKSPACE\uar-orchestrator\terraform\.terraform\modules\lambda_function_bg.lambda_function\package.py", line 1628, in <module>
│     main()
│   File "C:\Users\z004rc1n\Documents\WORKSPACE\uar-orchestrator\terraform\.terraform\modules\lambda_function_bg.lambda_function\package.py", line 1624, in main
│     exit(args.command(args))
│   File "C:\Users\z004rc1n\Documents\WORKSPACE\uar-orchestrator\terraform\.terraform\modules\lambda_function_bg.lambda_function\package.py", line 1423, in prepare_command
│     build_plan = bpm.plan(source_path, query)
│   File "C:\Users\z004rc1n\Documents\WORKSPACE\uar-orchestrator\terraform\.terraform\modules\lambda_function_bg.lambda_function\package.py", line 773, in plan
│     pip_requirements_step(
│   File "C:\Users\z004rc1n\Documents\WORKSPACE\uar-orchestrator\terraform\.terraform\modules\lambda_function_bg.lambda_function\package.py", line 675, in pip_requirements_step
│     raise RuntimeError(
│ RuntimeError: Python interpreter version equal to defined lambda runtime (python3.9) should be available in system PATH
│
│ State: exit status 1

I have spent a few days troubleshooting this issue, and I have tried the following methods. I am on Windows 11, using Git Bash, but I have also tried PowerShell.

I am using the python3.9 runtime and installed python 3.9.10 on my machine so that it would match - the python folder is python39 which should satisfy the requirement. I have manually added the executable to my System Environment Variable PATH and can confirm that it is accessible in PowerShell. I have manually added it to the PATH as well in my .bash_profile file and verified that it is accessible as well.

On both shells I type python --version and get back 3.9.10.

I have deleted the .terraform folder, re-initialized, ran plan which worked, but always fails on apply.

I have restarted my entire machine multiple times.

I have tried using virtual environments i.e. activate venv, check python version, then run terraform apply.

I ran terraform fmt on all files.

I set the DEBUG level to 3 using export TF_LAMBDA_PACKAGE_LOG_LEVEL=DEBUG3

which yielded nothing interesting, but I was able to confirm that the paths make sense:

  with module.lambda_function_bg.module.lambda_function.data.external.archive_prepare[0],
│   on .terraform\modules\lambda_function_bg.lambda_function\package.tf line 10, in data "external" "archive_prepare":
│   10:   program = [local.python, "${path.module}/package.py", "prepare"]
│
│ The data source received an unexpected error while attempting to execute the program.
│
│ Program: C:\Users\z004rc1n\venvs\add_user_to_jira_39\Scripts\python.exe
│ Error Message: [8696] prepare: QUERY: {
│   "artifacts_dir": "builds/add_user_to_jira/",
│   "docker": "",
│   "hash_extra": "",
│   "hash_extra_paths": "[]",
│   "paths": "{\"cwd\":\"C:/Users/z004rc1n/Documents/WORKSPACE/uar-orchestrator/terraform\",\"module\":\".terraform/modules/lambda_function_bg.lambda_function\",\"root\":\".\"}",
│   "recreate_missing_package": "true",
│   "runtime": "python3.9",
│   "source_path": "\"../lambdas/add_user_to_jira/\""
│ }

IMPORTANT NOTE: it works fine when requirements.txt is not present. I am only getting the failure when the requirements exist in the same directory as the lambda handler (main.py).

The Lambda code is extremely simple and exists of two files: main.py and requirements.txt in the same directory that is passed using source_path.

Please provide a clear and concise description of the issue you are encountering, and a reproduction of your configuration (see the examples/* directory for references that you can copy+paste and tailor to match your configs if you are unable to copy your exact configuration). The reproduction MUST be executable by running terraform init && terraform apply without any further changes.

If your request is for a new feature, please use the Feature request template.

⚠️ Note

Before you submit an issue, please perform the following first:

  1. Remove the local .terraform directory (! ONLY if state is stored remotely, which hopefully you are following that best practice!): rm -rf .terraform/ check
  2. Re-initialize the project root to pull down modules: terraform init check
  3. Re-attempt your terraform plan or apply and check if the issue still persists check

Versions

Reproduction Code [Required]

module "lambda_function" {
  source  = "terraform-aws-modules/lambda/aws"
  version = "4.13.0"

  depends_on = [
    aws_cloudwatch_log_group.lambda-log-group
  ]

  function_name                 = var.function_name
  description                       = var.description
  handler                             = var.handler
  runtime                            = var.runtime
  publish                             = true
  use_existing_cloudwatch_log_group = true
  attach_cloudwatch_logs_policy     = false
  create_role                       = false

  lambda_role = aws_iam_role.lambda-role.arn

  source_path = "../lambdas/${var.function_name}/"

  store_on_s3   = true
  s3_bucket     = var.s3_bucket_id
  artifacts_dir = "builds/${var.function_name}/"

  tags = var.tags
}

Steps to reproduce the behavior:

NO YES Created Module resource, ran plan (worked), ran apply --> got the error ## Expected behavior

Create deployment package with pip install step and upload to s3. It works fine when requirements.txt is not present.

Actual behavior

Fails with error above when requirements.txt exists in lambda source_path directory.

Terminal Output Screenshot(s)

Output included above, but full error is here again:

Planning failed. Terraform encountered an error while generating this plan.

╷
│ Error: External Program Execution Failed
│
│   with module.lambda_function_bg.module.lambda_function.data.external.archive_prepare[0],
│   on .terraform\modules\lambda_function_bg.lambda_function\package.tf line 10, in data "external" "archive_prepare":
│   10:   program = [local.python, "${path.module}/package.py", "prepare"]
│
│ The data source received an unexpected error while attempting to execute the program.
│
│ Program: C:\Users\z004rc1n\venvs\add_user_to_jira_39\Scripts\python.exe
│ Error Message: [8696] prepare: QUERY: {
│   "artifacts_dir": "builds/add_user_to_jira/",
│   "docker": "",
│   "hash_extra": "",
│   "hash_extra_paths": "[]",
│   "paths": "{\"cwd\":\"C:/Users/z004rc1n/Documents/WORKSPACE/uar-orchestrator/terraform\",\"module\":\".terraform/modules/lambda_function_bg.lambda_function\",\"root\":\".\"}",
│   "recreate_missing_package": "true",
│   "runtime": "python3.9",
│   "source_path": "\"../lambdas/add_user_to_jira/\""
│ }
│ Traceback (most recent call last):
│   File "C:\Users\z004rc1n\Documents\WORKSPACE\uar-orchestrator\terraform\.terraform\modules\lambda_function_bg.lambda_function\package.py", line 1628, in <module>
│     main()
│   File "C:\Users\z004rc1n\Documents\WORKSPACE\uar-orchestrator\terraform\.terraform\modules\lambda_function_bg.lambda_function\package.py", line 1624, in main
│     exit(args.command(args))
│   File "C:\Users\z004rc1n\Documents\WORKSPACE\uar-orchestrator\terraform\.terraform\modules\lambda_function_bg.lambda_function\package.py", line 1423, in prepare_command
│     build_plan = bpm.plan(source_path, query)
│   File "C:\Users\z004rc1n\Documents\WORKSPACE\uar-orchestrator\terraform\.terraform\modules\lambda_function_bg.lambda_function\package.py", line 773, in plan
│     pip_requirements_step(
│   File "C:\Users\z004rc1n\Documents\WORKSPACE\uar-orchestrator\terraform\.terraform\modules\lambda_function_bg.lambda_function\package.py", line 675, in pip_requirements_step
│     raise RuntimeError(
│ RuntimeError: Python interpreter version equal to defined lambda runtime (python3.9) should be available in system PATH
│
│ State: exit status 1

Additional context

bpgould commented 1 year ago

Additional Context: I am wrapping in my own module. My module is defined like this:

variable "function_name" {
  type        = string
  description = "The name of the Lambda function"
}

variable "description" {
  type        = string
  description = "The description of the Lambda function"
}

variable "handler" {
  type        = string
  description = "The handler function for the Lambda function"
}

variable "runtime" {
  type        = string
  description = "The runtime for the Lambda function"
}

variable "s3_bucket_id" {
  type        = string
  description = "The terraform id of the s3 bucket created outside of the module that will be used to store the Lambda deployment package artifacts"
}

variable "additional_iam_statements" {
  type = list(object({
    Sid      = string
    Effect   = string
    Action   = list(string)
    Resource = list(string)
  }))
  default     = []
  description = "Additional IAM statements to include in the Lambda function's policy"
}

variable "tags" {
  type        = map(string)
  description = "The tags to apply to all resources created by the module"
  default     = {}
}

resource "aws_iam_policy" "lambda-policy" {
  name        = "${var.function_name}-policy"
  description = "Allows Lambda function to execute"
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = concat(
      [
        {
          Sid = "AllowLambdaToWriteLogs"
          Action = [
            "logs:CreateLogGroup",
            "logs:CreateLogStream",
            "logs:PutLogEvents"
          ]
          Effect   = "Allow"
          Resource = "${aws_cloudwatch_log_group.lambda-log-group.arn}"
        }
      ],
      [for statement in var.additional_iam_statements : {
        Sid      = statement.Sid
        Effect   = statement.Effect
        Action   = statement.Action
        Resource = statement.Resource
      }]
    )
  })
}

resource "aws_iam_role" "lambda-role" {
  name = "${var.function_name}-role"

  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-policy-attachment" {
  policy_arn = aws_iam_policy.lambda-policy.arn
  role       = aws_iam_role.lambda-role.name
}

resource "aws_cloudwatch_log_group" "lambda-log-group" {
  name = "/aws/lambda/${var.function_name}"
}

module "lambda_function" {
  source  = "terraform-aws-modules/lambda/aws"
  version = "4.13.0"

  depends_on = [
    aws_cloudwatch_log_group.lambda-log-group
  ]

  function_name                     = var.function_name
  description                       = var.description
  handler                           = var.handler
  runtime                           = var.runtime
  publish                           = true
  use_existing_cloudwatch_log_group = true
  attach_cloudwatch_logs_policy     = false
  create_role                       = false

  lambda_role = aws_iam_role.lambda-role.arn

  source_path = "../lambdas/${var.function_name}/"

  store_on_s3   = true
  s3_bucket     = var.s3_bucket_id
  artifacts_dir = "builds/${var.function_name}/"

  tags = var.tags
}

output "function_arn" {
  value = module.lambda_function.lambda_function_arn
}

and I am using it like this:

module "lambda_function_bg" {
  source = "../modules/bennett_lambda"

  function_name = "add_user_to_jira"
  handler       = "main.lambda_handler"
  runtime       = "python3.9"
  s3_bucket_id  = aws_s3_bucket.uar-lambda-artifacts.id

  description = "Lambda function for UAR to create a new user in Jira"

  additional_iam_statements = [
    {
      Sid      = "SSMParameterAccess"
      Effect   = "Allow"
      Action   = ["ssm:GetParameter"]
      Resource = ["${aws_ssm_parameter.jira_token.arn}"]
    },
    {
      Sid    = "AllowUseOfSpecificCMK"
      Effect = "Allow"
      Action = [
        "kms:Decrypt",
        "kms:DescribeKey"
      ]
      Resource = ["${aws_kms_key.parameter_store.arn}"]
    }
  ]
}

and my directory structure is this:

image
bpgould commented 1 year ago

I also manually aliased python3 to the absolute path of the 3.9 executable in case the module script was calling python via python3 and not python. Still, same error.

spectorar commented 1 year ago

I was just running into a similar issue when trying to run on Github actions. I ended up just needing to install python. But I think what may be going on here is that this module's package.py is actually looking for an executable on the path that matches the runtime exactly. So for example, if your runtime is python3.9 it's looking for python3.9 on the path, not python or python3. You can see it here, and command is set to runtime.

Try aliasing python3.9 to python3 or python.

bpgould commented 1 year ago

I was just running into a similar issue when trying to run on Github actions. I ended up just needing to install python. But I think what may be going on here is that this module's package.py is actually looking for an executable on the path that matches the runtime exactly. So for example, if your runtime is python3.9 it's looking for python3.9 on the path, not python or python3. You can see it here, and command is set to runtime.

Try aliasing python3.9 to python3 or python.

I created the alias alias python3.9="python" and tested via python3.9 --version --> 3.9.10. Ran terraform apply --> same error

bpgould commented 1 year ago

Additional new information: The error comes from package.py line 684:

if not query.docker and not shutil.which(command):
                    shutiloutput = shutil.which(command)
                    raise RuntimeError(
                        "Python interpreter version equal "
                        "to defined lambda runtime ({}) should be "
                        "available in system PATH".format(command),
                    )

To troubleshoot, I changed it to:

if not query.docker and not shutil.which(command):
                    shutiloutput = shutil.which(command)
                    raise RuntimeError(
                        f"shutiloutput:{shutiloutput}",
                        "Python interpreter version equal "
                        "to defined lambda runtime ({}) should be "
                        "available in system PATH".format(command),
                    )

and I see that shutil.which() returns None

So, I go to my terminal and do the following:


$ python -i
>>> import shutil
>>> shutil.which("python3.9")
>>> print(shutil.which("python3.9"))
None
>>> print(shutil.which("python"))
C:\Users\me\AppData\Local\Programs\Python\Python39\python.EXE
>>> print(shutil.which("python3"))
C:\Users\me\AppData\Local\Microsoft\WindowsApps\python3.EXE
>>> print(shutil.which("python3.9"))
None

OK, so it is explicit what the issue is, but if I rename the folder from `Python39` to `Python3.9` and update my PATH, it does not appear to be valid. Even when running `print(shutil.which("python3.9"))` Now I still get `None`
spectorar commented 1 year ago

I'm not a windows guy, but I don't think it's based on the folder name but on the name of the executable. Try updating python.EXE to python3.9.EXE and make sure the folder containing that executable is on your path.

bpgould commented 1 year ago

I'm not a windows guy, but I don't think it's based on the folder name but on the name of the executable. Try updating python.EXE to python3.9.EXE and make sure the folder containing that executable is on your path.

It works! Woohoo, thank you!

I renamed the actual executable to python3.9.exe and used the standard convention of adding the folder to my PATH.

This should be documented though because this is unusual behavior because the code uses shutil.which(runtime) and in my example shutil.which("python3.9") because of this, even an alias of alias python3.9="python" will not work. I wonder if it would be more user friendly to use shutil.which("python") then check that the version matches via subprocess command python --version or some local python config file that I am not familiar with.

Thanks again!

github-actions[bot] commented 1 year ago

I'm going to lock this issue because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active issues. If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.