30

I´m using Terraform to create a script that builds some EC2 Servers and a MySQL RDS (using AWS Amazon Provider).

Is there a way to execute a SQL script on this created RDS (i want to create users, tables, etc)?

Thanks in advance,

Att,

7 Answers7

25

Like this solution, You can also avoid instance setup time/cost by using your own machine with local-exec IF your RDS database is publicly available and you have setup ingress to allow your machine to connect. Then, with credentials stored securely in your environment, you would just do something like:

resource "null_resource" "db_setup" {

  # runs after database and security group providing external access is created
  depends_on = ["aws_db_instance.your_database_instance", "aws_security_group.sg_allowing_external_access"]

    provisioner "local-exec" {
        command = "database connection command goes here"
        environment = {
          # for instance, postgres would need the password here:
          PGPASSWORD = "${var.database_admin_password}"
        }
    }
}

Keep in mind that passwords and other sensitive variables can be input into terraform separately.

ecoe
  • 4,994
  • 7
  • 54
  • 72
  • 1
    Please note that, as mentioned by sub-OP, this means that "public" access to the database is required(either by a VPN or allowing connections to DB from the internet). – blahblah Apr 25 '19 at 06:35
  • Needs equal sign after `environment` to avoid "Unsupported block type" error? – avriis Nov 09 '21 at 13:20
  • How will this configuration change, if I have my RDS in private subnet..? – Sanyam Grover Nov 11 '21 at 11:45
11

Building upon ecoe's answer:

For future readers using a Postgres RDS instance, this is what worked for me (you must have psql installed on your machine):

variable "db_username" {
  type = string
}

variable "db_password" {
  type = string
}

resource "null_resource" "db_setup" {

  provisioner "local-exec" {

    command = "psql -h host_name_here -p 5432 -U \"${var.db_username}\" -d database_name_here -f \"path-to-file-with-sql-commands\""

    environment = {
      PGPASSWORD = "${var.db_password}"
    }
  }
}
rishikarri
  • 3,462
  • 2
  • 13
  • 13
  • What about timeouts, if the db migrations take too long for instance? It seems like health_checks could fail and terminate the task. Is this not an issue with this approach? – Jordan Oct 10 '19 at 16:28
  • 2
    Hmm this is a good point - this approach assumes you already have a database that is up and running. If you are building your db and running this snippet on the same "terraform apply", this may not work because the db might not be ready to receive SQL commands. To avoid this, make sure your RDS instance is set up prior to running this. Does that answer your question? – rishikarri Oct 11 '19 at 14:48
  • Is there any way to execute SQL script within VPC or private RDS instance? – Rounak Jain Nov 02 '21 at 17:21
  • @RounakJain you can open an ssh tunnel using SSM manager to a bastion that lives within the same private subnet as the RDS. That will allow you to run `psql` command from your machine without exposing ur database to the public internet. Here's an [article](https://www.padok.fr/en/blog/aws-ssh-bastion) that may help setup this solution. – Aymen Ben Tanfous Apr 28 '22 at 10:55
2

You'll want to use terraform remote exec in a provisioner resource. Getting db credentials to the instance is a different story. You can use something like consul or pull the creds from s3 and have the script parse.

resource "aws_instance" "web" {


  provisioner "remote-exec" {
    inline = [
      "chmod +x /tmp/script.sh",
      "/tmp/script.sh args",
    ]
  }
}

https://www.terraform.io/docs/provisioners/remote-exec.html#script

strongjz
  • 4,271
  • 1
  • 17
  • 27
0

Elaborating on the previous answers with a MySQL version.

MySQL version with file

resource "null_resource" "db_setup" {
  depends_on = [module.db, aws_security_group.rds_main, aws_default_security_group.default]
  provisioner "local-exec" {
    command = "mysql --host=${module.db.this_db_instance_address} --port=${var.dbport} --user=${var.dbusername} --password=${var.dbpassword} --database=${var.dbname} < ${file(${path.module}/init/db_structure.sql)}"
  }
}

MySQL version with local_file

data "local_file" "sql_script" {
  filename = "${path.module}/init/db_structure.sql"
}

resource "null_resource" "db_setup" {
  depends_on = [module.db, aws_security_group.rds_main, aws_default_security_group.default]
  provisioner "local-exec" {
    command = "mysql --host=${module.db.this_db_instance_address} --port=${var.dbport} --user=${var.dbusername} --password=${var.dbpassword} --database=${var.dbname} < ${data.local_file.sql_script.content}"
  }
}
0

my database is in private subnets and the security groups allow only the bastion to connect.
I have an SQL file that creates the different users and schemas.
There can be better ways, but this works for my use case.

the SQL file has passwords which I pass through terraform variables -

data "template_file" "init_db" {
  template = file("./init-db.sql")

  vars = {
    account_service_db_password       = var.account_service_db_password
    account_service_db_admin_password = var.account_service_db_admin_password

    question_service_db_password       = var.question_service_db_password
    question_service_db_admin_password = var.question_service_db_admin_password
  }
}

now I have to copy the SQL file and then execute commands -

resource "aws_instance" "bastion" {
  # ...
  instance_type = "t2.micro"
  # ...

  connection {
    type        = "ssh"
    host        = self.public_ip
    user        = "ec2-user"
    private_key = file("./n_virginia.pem")
  }

  provisioner "file" {
    content     = data.template_file.init_db.rendered
    destination = "init-db.sql"
  }

  provisioner "remote-exec" {
    inline = [
      "sudo yum update -y",
      "sudo yum install mysql -y",
      "mysql -u ${var.db_root_username} -h ${aws_db_instance.main.address} -p${var.db_root_password} < init-db.sql"
    ]
  }
}
keemahs
  • 780
  • 11
  • 14
0

I answered a similar question, this answer works for creating users, databases, roles, etc...

Terraform's AWS provider only has functionality to provision the platform that hosts the MySQL DBMS (Aurora). Creating schemas, databases, roles etc... has to be done using the DBMS (running SQL queries).

In order to do this through terraform, you'll have to use a provider created for MySQL (a quick google search found petoju/mysql - please complete the nessecary due dilligence when using open source libraries in a production environment!).

Best practices for running database clusters in the cloud call for isolating the cluster in it's own private network (AWS VPC) with tightly controlled rules for incoming and outgoing traffic. This poses a problem as terraform will need to connect directly to MySQL in order to provision the resources.

If you're running terraform from a CI/CD pipeline (GH Actions, CircleCI, etc...) that has limitied networking configuration, or on a local machine. You can create a 'bastion' ec2 instance that lives in a subnet with access to the outside internet. You can then setup an SSH tunnel to that instance from the CI runner that you can proxy the request through using the provider's proxy function or terraform's ALL_PROXY environment variable.

If you have full control of the CI/CD runners (GH Actions self hosted runners) and they are running in another AWS VPC, you can setup VPC peering which will allow the runners to directly communicate with the Aurora clusters.

Creating/Modifying tables should be controlled at the application layer, there are many platforms/libraries that allow you to create and manage migrations properly (such as liquibase, or more framework specific tools like Umzug for NodeJS).

Managing table migrations through terraform is going to lead to a lot of headache.

Jesse
  • 61
  • 4
0

Found a viable solution here.

Uses shell also, but does not require a local psql client, nor ability to connect to psql server from localhost because it uses aws rds-data execute-statement to actually execute SQL statements.

But for Aurora only, not a bare RDS psql.

zed_0xff
  • 32,417
  • 7
  • 53
  • 72