hashicorp / terraform

Terraform enables you to safely and predictably create, change, and improve infrastructure. It is a source-available tool that codifies APIs into declarative configuration files that can be shared amongst team members, treated as code, edited, reviewed, and versioned.
https://www.terraform.io/
Other
42.49k stars 9.51k forks source link

panic: runtime error: invalid memory address or nil pointer dereference #21770

Closed tompahoward closed 5 years ago

tompahoward commented 5 years ago

Terraform Version

Terraform v0.12.2
+ provider.aws v2.15.0
+ provider.cloudflare v1.15.0
+ provider.mongodbatlas v1.1.0

Terraform Configuration Files

main.tf

terraform {
  backend "remote" {
    organization = "org"

    workspaces {
      prefix = "superlife-"
    }
  }
}

versions.tf

terraform {
  required_version = ">= 0.12"
}

provider.tf

provider "aws" {
  version = "~> 2.12"
  region  = "ap-southeast-2"

  //Set AWS_ACCESS_KEY_ID & AWS_SECRET_ACCESS_KEY env variables
}

provider "cloudflare" {
  version = "~> 1.15"

  //Set CLOUDFLARE_TOKEN and CLOUDFLARE_EMAIL env variables
}

provider "mongodbatlas" {
  version = "~> 1.1.0"
  // Set MONGODB_ATLAS_USERNAME and MONGODB_ATLAS_API_KEY env variables
}

superlife.tf

variable "pvt_key" {
}

variable "pub_key" {
}

variable "docker_pass" {
  type = string
}

variable "docker_user" {
  type = string
}

variable "PORT" {
  type = string
}

variable "SSL_PORT" {
  type = string
}

resource "aws_key_pair" "deployer" {
  key_name   = "${terraform.workspace}_deployer_key"
  public_key = file(var.pub_key)
}

resource "aws_security_group" "allow_ssh" {
  name        = "${terraform.workspace}_allow_ssh"
  description = "Allow ssh inbound traffic"

  ingress {
    # TLS (change to whatever ports you need)
    from_port = 22
    to_port   = 22
    protocol  = "tcp"

    # TODO: Please restrict your ingress to only necessary IPs and ports.
    # Opening to 0.0.0.0/0 can lead to security vulnerabilities.
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "ssh"
  }
}

resource "aws_security_group" "allow_http" {
  name        = "${terraform.workspace}_allow_http"
  description = "Allow http inbound traffic"

  ingress {
    # TLS (change to whatever ports you need)
    from_port = 80
    to_port   = 80
    protocol  = "tcp"

    # TODO: Please restrict your ingress to only necessary IPs and ports.
    # Opening to 0.0.0.0/0 can lead to security vulnerabilities.
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "http"
  }
}

resource "aws_security_group" "allow_https" {
  name        = "${terraform.workspace}_allow_https"
  description = "Allow https inbound traffic"

  ingress {
    # TLS (change to whatever ports you need)
    from_port = 443
    to_port   = 443
    protocol  = "tcp"

    # TODO: Please restrict your ingress to only necessary IPs and ports.
    # Opening to 0.0.0.0/0 can lead to security vulnerabilities.
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "https"
  }
}

resource "aws_security_group" "allow_outbound_tls_mail" {
  name        = "${terraform.workspace}_allow_outbound_tls_mail"
  description = "Allow tls and mail outbound traffic"

  egress {
    # TLS (change to whatever ports you need)
    from_port = 443
    to_port   = 443
    protocol  = "tcp"

    # TODO: Please restrict your egress to github only.
    # See https://help.github.com/en/articles/about-githubs-ip-addresses
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    # TLS (change to whatever ports you need)
    from_port = 465
    to_port   = 465
    protocol  = "tcp"

    # TODO: Please restrict your egress to github only.
    # See https://help.github.com/en/articles/about-githubs-ip-addresses
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "allow_outbound_tls_mail"
  }
}

resource "aws_security_group" "allow_outbound_http" {
  name        = "${terraform.workspace}_allow_outbound_http"
  description = "Allow http outbound traffic"

  egress {
    # TLS (change to whatever ports you need)
    from_port = 80
    to_port   = 80
    protocol  = "tcp"

    # TODO: Please restrict your egress to github only.
    # See https://help.github.com/en/articles/about-githubs-ip-addresses
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "allow_outbound_http"
  }
}

resource "aws_eip" "superlife_public_ip" {
  lifecycle {
    create_before_destroy = true
  }
}

output "eip" { value = "${aws_eip.superlife_public_ip.public_ip}" }

resource "aws_eip_association" "superlife_public_ip_assoc" {
  instance_id   = aws_instance.superlife.id
  allocation_id = aws_eip.superlife_public_ip.id

  lifecycle {
    create_before_destroy = true
  }
}

resource "cloudflare_record" "superlife" {
  domain  = "org.com.au"
  name    = "${terraform.workspace}-superlife"
  value   = aws_eip.superlife_public_ip.public_ip
  type    = "A"
  proxied = true

  lifecycle {
    create_before_destroy = true
  }
}

output "hostname" { value = "${cloudflare_record.superlife.hostname}" }

data "aws_region" "current" {
}

# TODO have a globals terraform script that
# does things we only want to do once across all
# environments. That's were we would create
# mongodbatlas_project.superlife 
data "mongodbatlas_project" "superlife" {
  name = "superlife"
}

resource "mongodbatlas_cluster" "superlife" {
  name                  = "${terraform.workspace}-superlife"
  group                 = "${data.mongodbatlas_project.superlife.id}"
  mongodb_major_version = "4.0"
  provider_name         = "AWS"
  region                = "AP_SOUTHEAST_2"
  size                  = "M10"
  backup                = true

  lifecycle {
    create_before_destroy = true
  }
}

output "mongo_uri" { value = "${mongodbatlas_cluster.superlife.mongo_uri}" }
output "mongo_uri_with_options" { value = "${mongodbatlas_cluster.superlife.mongo_uri_with_options}" }
output "srv_address" { value = "${mongodbatlas_cluster.superlife.srv_address}" }
output "replication_spec" { value = "${mongodbatlas_cluster.superlife.replication_spec}" }

resource "aws_instance" "superlife" {
  depends_on    = ["mongodbatlas_cluster.superlife"]
  ami           = "ami-0b76c3b150c6b1423"
  instance_type = "t2.medium"
  monitoring    = "true"
  key_name      = "${terraform.workspace}_deployer_key"

  root_block_device {
    volume_size = "16"
  }

  security_groups = [
    "${terraform.workspace}_allow_ssh",
    "${terraform.workspace}_allow_http",
    "${terraform.workspace}_allow_https",
    "${terraform.workspace}_allow_outbound_tls_mail",
    "${terraform.workspace}_allow_outbound_http",
  ]

  tags = {
    Name = "superlife-${terraform.workspace}"
  }

  lifecycle {
    create_before_destroy = true
  }

  connection {
    host        = coalesce(self.public_ip, self.private_ip)
    user        = "ubuntu"
    type        = "ssh"
    private_key = file(var.pvt_key)
    timeout     = "2m"
  }

  provisioner "local-exec" {
    command = "tar zcvf proxy.tgz -C .. proxy && tar zcvf cache.tgz -C .. cache"
  }

  provisioner "file" {
    source      = "../docker-compose.yml"
    destination = "/home/ubuntu/docker-compose.yml"
  }

  provisioner "file" {
    source      = "../docker-compose.override.yml"
    destination = "/home/ubuntu/docker-compose.override.yml"
  }

  # environment config

  provisioner "file" {
    source      = "../common.env"
    destination = "/home/ubuntu/common.env"
  }

  provisioner "file" {
    source      = "../common.env"
    destination = "/home/ubuntu/.env"
  }

  provisioner "file" {
    source      = "../common.bank.env"
    destination = "/home/ubuntu/common.bank.env"
  }
  provisioner "file" {
    source      = "../common.rewards.env"
    destination = "/home/ubuntu/common.rewards.env"
  }
  provisioner "file" {
    source      = "../${terraform.workspace}.env"
    destination = "/home/ubuntu/${terraform.workspace}.env"
  }
  provisioner "file" {
    source      = "../${terraform.workspace}.bank.env"
    destination = "/home/ubuntu/${terraform.workspace}.bank.env"
  }
  provisioner "file" {
    source      = "../${terraform.workspace}.rewards.env"
    destination = "/home/ubuntu/${terraform.workspace}.rewards.env"
  }

  # proxy config and proxied services cache

  provisioner "file" {
    source      = "proxy.tgz"
    destination = "/home/ubuntu/proxy.tgz"
  }
  provisioner "file" {
    source      = "cache.tgz"
    destination = "/home/ubuntu/cache.tgz"
  }
  provisioner "remote-exec" {
    inline = [
      "ls",
      "sudo apt-get --yes update",
      "sudo apt-get --yes install apt-transport-https ca-certificates curl gnupg-agent software-properties-common",
      "curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -",
      "sudo apt-key fingerprint 0EBFCD88",
      "sudo add-apt-repository \"deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\"",
      "sudo apt-get --yes update",
      "sudo apt-get --yes install docker-ce docker-ce-cli containerd.io",
      "docker --version",
      "sudo systemctl enable docker",
      "sudo systemctl start docker",
      "sudo groupadd docker",
      "sudo usermod -aG docker $USER",
      "curl -L \"https://github.com/docker/compose/releases/download/1.24.0/docker-compose-$(uname -s)-$(uname -m)\" -o /home/ubuntu/docker-compose",
      "chmod +x /home/ubuntu/docker-compose",
      "/home/ubuntu/docker-compose --version",
    ]
  }
  provisioner "remote-exec" {
    # unzip the files needed to build the docker images
    inline = [
      ...
    ]
  }
  provisioner "local-exec" {
    command = <<EOT
fail_count=1

while true
do
  wget --header 'Accept: application/json' --tries=1 --no-check-certificate --timeout=3 -S -q 'https://${self.public_ip}/kitchenSink' -O /dev/null

  if [ $? -eq 0 ] ; then
    echo "$(date -u) ================================================================================"
    echo "$(date -u) Server available"
    echo "$(date -u)   Addresses:"
    echo "$(date -u)     elastic: ${aws_eip.superlife_public_ip.public_ip}"
    echo "$(date -u)     hostname: ${cloudflare_record.superlife.hostname}"
    echo "$(date -u) ================================================================================"    
    exit 0
  else
    if [ $fail_count -eq 61 ]; then
      echo "$(date -u) Server unavailable"
      exit 2
    else
      echo "$(date -u) Attempt $${fail_count}/60: Server not yet available"
      sleep 3
      fail_count=$(($${fail_count} + 1))
    fi
  fi
done

EOT

  }
}

Debug Output

https://gist.github.com/tompahoward/3776b869694c75630fcfb9a9962e9dc8

Crash Output

https://gist.github.com/tompahoward/e2d4e41dc99e263b7dd0e8ea1e72e30e

Expected Behavior

Not crash

Actual Behavior

Crashed

Steps to Reproduce

Workspace is in a strange stage. Previously did and plan and apply. Now when trying to taint an was instance, it fails with

Resource instance aws_instance.superlife is currently part-way through a
create_before_destroy replacement action. Run "terraform apply" to complete
its replacement before tainting it.

So trying to figure out what's going on. ran terraform refresh and terraform show. The later resulted in the crash.

Additional Context

References

tompahoward commented 5 years ago

Here's with TF_LOG=trace https://gist.github.com/tompahoward/0e8711449b48bcd24e0ca273ab4d7df8

tompahoward commented 5 years ago

It's also worth noting that the workspace for this is used as a smoke-test for the deployment. For every commit that build's successfully we do deploy (using plan and apply) and then we destroy it (using plan --destroy and apply). As I haven't see this error with other environments, I'm wondering if it's the constant create and destroy that triggers this defect.

OJFord commented 5 years ago

I caused the same by manually deleting (Vultr) servers that were listed deposed in tfstate. Removing them from state (manually) sorted it.

ghost commented 5 years 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.