Build MySQL-based 3-Tier web service with Terraform
Deploy a 3-Tier web service by creating a KakaoCloud network, web and application servers, load balancers, and MySQL with Terraform.
- Estimated time: 40–60 minutes, including MySQL provisioning
- Recommended operating systems: macOS, Ubuntu
- Terraform CLI version:
1.14.0or later, the common baseline for both labs - Prerequisites
- IAM access key and resource management permissions in the target project
- Registered key pair and its private key file
- Administrator public IP for bastion host access
If you are new to Terraform, complete Provision and update VM with Terraform first. That tutorial covers CLI installation, IAM credential preparation, and the VM create, update, and delete workflow.
About this scenario
In this tutorial, you use Terraform to configure the network, servers, load balancers, and MySQL required for the KakaoCloud Library web service.
Create two web servers and two application servers, connect them to their respective load balancers, and deploy containers automatically using user_data. After deployment, access the service through the web load balancer's public IP to verify the result.
The following diagram shows the main resources created with Terraform and the web request flow. The upper section shows service requests, and the lower section shows administrative access through the bastion host.
Internet
| HTTP:80
v
+-- VPC: tutorial (10.0.0.0/16) / kr-central-2-a -----+
| |
| tutorial-main [Public ALB: tutorial-web-lb] |
| | HTTP:80 |
| +-------+-------+ |
| v v |
| tutorial-web [Web VM 1] [Web VM 2] |
| | | |
| +-------+-------+ |
| | TCP:8080 |
| v |
| tutorial-alb [Internal NLB: tutorial-app-lb]|
| | TCP:8080 |
| +-------+-------+ |
| v v |
| tutorial-app [App VM 1] [App VM 2] |
| | | |
| +-------+-------+ |
| | TCP:3306 |
| v |
| tutorial-mysql [Managed MySQL: tutorial-mysql]|
| |
| tutorial-main [Bastion: tutorial-bastion] |
+-----------------------------------------------------+
Admin (admin_cidr)
| SSH:22 / Admin UI:81 / Forwarding:10000-10010
v
Bastion -- TCP forwarding --> Web / App VM SSH:22
The web and application servers are targets for their respective load balancers. The bastion host relays administrative connections and is separate from the web request path. In the diagram, Web VM 1/2 represents tutorial-web-1/2, and App VM 1/2 represents tutorial-app-1/2.
- The example deploys to a single availability zone and uses one MySQL Primary instance. Although it creates two servers for each of the web and application tiers, it does not include a configuration for availability zone failures.
- For a walkthrough that creates resources directly in the console, see Configure VM-based web service.
Before you start
If you completed Provision and update VM with Terraform, delete the VM you created and prepare a new working directory for this example.
Prepare these items in the project where you will run the example. Terraform creates the VPC and subnets, so you do not need to create them in advance.
| Item | Preparation |
|---|---|
| Project and permissions | A project and IAM roles that allow creation, lookup, and deletion of VPC, Virtual Machine, Load Balancer, and MySQL resources |
| IAM access key | An IAM access key ID and secret access key issued for the target project |
| Key pair | A key pair registered in the target project and its private key file |
| Local tools | An environment where you can run git, curl, and ssh |
| Administrator IP | The public IP of the local environment that will access the bastion, in CIDR notation, such as 203.0.113.10/32 |
The example looks up a registered key pair and does not create a new one. The default name is kr2-cloudtech. To use a different key pair, set keypair_name in terraform.tfvars.
Resource plan
With the default variable values, the example creates the following resources in the kr-central-2-a availability zone of the kr-central-2 region. You can change the tutorial name prefix using project_name.
Network
The example creates one VPC and five subnets for different purposes.
| Type | Name | CIDR | Purpose |
|---|---|---|---|
| VPC | tutorial | 10.0.0.0/16 | VPC containing all resources |
| Subnet (main) | tutorial-main | 10.0.0.0/20 | Bastion and web load balancer |
| Subnet (web) | tutorial-web | 10.0.16.0/20 | Web tier instances |
| Subnet (alb) | tutorial-alb | 10.0.32.0/20 | Internal application load balancer |
| Subnet (app) | tutorial-app | 10.0.48.0/20 | Application tier instances |
| Subnet (mysql) | tutorial-mysql | 10.0.64.0/20 | Managed MySQL |
The example creates an internet gateway (IGW), attaches it to the VPC, and adds a 0.0.0.0/0 → IGW default route to the route table. Check network.tf for the network and security group rules for each tier.
Instances
The example creates one bastion host and four web and application servers.
| Role | Name | Instance type | Public IP | Image |
|---|---|---|---|---|
| Bastion | tutorial-bastion | m2a.large | Y | Ubuntu 20.04 |
| WEB1 | tutorial-web-1 | m2a.large | N | Ubuntu 20.04 |
| WEB2 | tutorial-web-2 | m2a.large | N | Ubuntu 20.04 |
| WAS1 | tutorial-app-1 | m2a.large | N | Ubuntu 20.04 |
| WAS2 | tutorial-app-2 | m2a.large | N | Ubuntu 20.04 |
The web servers serve a React app through Nginx, and the application servers run a Spring Boot app. cloud-init installation and build tasks continue after instance creation, so wait until the containers are ready.
Load balancers
The example creates two load balancers for external and inter-tier requests.
| Type | Name | Public IP | Purpose |
|---|---|---|---|
| EXT-LB | tutorial-web-lb | Y | Distribute requests to web servers with ALB, HTTP:80 |
| INT-LB | tutorial-app-lb | N | Distribute requests to application servers with NLB, TCP:8080 |
Database
The example creates a managed MySQL instance group with this configuration.
| Type | Name | Configuration |
|---|---|---|
| MySQL | tutorial-mysql | One Primary instance, 100 GB data disk, 100 GB log disk, backups disabled |
Public IPs are attached to the web load balancer and bastion host. Web and application instances do not have public IPs. The bastion's SSH (22), admin UI (81), and port forwarding range (10000–10010) accept connections only from the IP range specified by admin_cidr.
Getting started
Check the Terraform environment and prepare the example, then create resources and verify the web service. After the lab, delete the resources you created.
Step 1. Check execution environment
Follow the installation instructions in the VM tutorial to prepare Terraform CLI 1.14.0 or later and git, then check their versions.
terraform version
git --version
Use the Provider version in this example's versions.tf. Do not copy .tf files, the .terraform directory, or state files from the VM tutorial. Run subsequent commands in the downloaded kakaocloud-3tier-terraform directory.
Step 2. Download example and configure inputs
-
Download the Terraform 3-Tier example.
Download examplegit clone --single-branch --branch feat/terraform-3tier-example https://github.com/kakaoenterprise/kakaocloud-tutorials.git kakaocloud-3tier-terraformThe configuration is divided into these files. Check
variables.tffor all variables andterraform.tfvars.examplefor example input values.File What to check versions.tf,variables.tfCLI and Provider requirements and input variables data.tf,locals.tf,keypair.tfImage, instance type, and key pair lookups and selection of lookup results network.tfVPC, subnets, internet gateway, routes, and security groups mysql.tfMySQL creation, endpoint wait, and lookup lb_app.tf,app.tfInternal NLB and application instances lb_web.tf,web.tfPublic ALB and web instances bastion.tf,outputs.tfBastion host and deployment outputs -
Change to the example directory.
Change working directorycd ./kakaocloud-3tier-terraform -
Set KakaoCloud credentials as environment variables. Enter the values prepared in Issue IAM access key. Do not write credentials to
terraform.tfvars. Set them again if you unset them after the VM tutorial or opened a new terminal.export TF_VAR_application_credential_id="${ACCESS_KEY}"
export TF_VAR_application_credential_secret="${SECRET_ACCESS_KEY}"환경변수 설명 ACCESS_KEY🖌︎ IAM access key ID SECRET_ACCESS_KEY🖌︎ Secret access key -
Copy the variable file.
Prepare variable filecp terraform.tfvars.example terraform.tfvars -
Edit
terraform.tfvarsand enter these values. Foradmin_cidr, use your current public IP followed by/32. Do not reuse the password in the example file.admin_cidr = "${ADMIN_CIDR}"
mysql_database_user_password = "${MYSQL_PASSWORD}"
keypair_name = "${KEYPAIR_NAME}"환경변수 설명 ADMIN_CIDR🖌︎ Administrator public IP in CIDR notation, e.g. 203.0.113.10/32 MYSQL_PASSWORD🖌︎ MySQL administrator password KEYPAIR_NAME🖌︎ Key pair name registered in the project Variable Value admin_cidrIP range allowed to access the bastion for administration mysql_database_user_passwordRequired variable with no default. Enter a value that meets the MySQL password policy keypair_nameRegistered key pair name. Defaults to kr2-cloudtechwhen omitted
terraform.tfvars stores the password in plain text. Terraform state and plan files can also contain sensitive information. Do not include terraform.tfvars, terraform.tfstate*, saved plan files, or private keys in version control.
Step 3. Review connections and create resources
First, trace how values pass through the configuration. Terraform determines execution order from resource references and depends_on, rather than filenames.
| Connection | What to check |
|---|---|
| Network → instances | Each instance's subnets references a subnet ID created in network.tf |
| MySQL → application | The endpoint looked up in mysql.tf is passed to MYSQL_HOST in app.tf |
| Internal NLB → web servers | SERVER_ENDPOINT in web.tf receives the internal NLB's private VIP and port 8080 |
| Instances → load balancers | Each load balancer's target group registers instances from its tier |
| Created resources → outputs | outputs.tf exposes connection addresses and instance IPs |
Validate the configuration, review the execution plan, and create resources from the example directory.
-
Initialize the current working directory.
Initialize working directoryterraform init -
Validate the configuration's syntax and references and review the execution plan. Verify that the resources and inputs match your intended configuration.
Review planterraform fmt -check
terraform validate
terraform planVerify that the plan does not delete or replace existing resources. With default values, it creates one VPC, five subnets, four security groups, five VMs, two load balancers, one MySQL instance group, and two public IPs. The plan also includes listeners, target groups, routes, and other resources, so the total count is higher than this list.
-
Apply the execution plan. Review the changes and enter
yesat the approval prompt.Apply configurationterraform apply
Managed MySQL may reach AVAILABLE before an endpoint is provided. The example waits two minutes after creating the instance group and looks up the endpoint again. If it is empty, execution stops before creating application instances. Check that the endpoint is available in the console, then run terraform apply again.
Step 4. Verify deployment
After resource creation, check the results in the console and browser. Container builds on web and application instances may continue after terraform apply finishes.
-
In Load Balancer, verify that
tutorial-web-lb(public ALB) andtutorial-app-lb(internal NLB) have been created. -
In Virtual Machine, verify that one bastion host, two web instances, and two application instances have been created. For their names, see Instances.
-
In MySQL, check the state and endpoint of the
tutorial-mysqlinstance group. After a successfulterraform apply, the endpoint should match this output.Check endpoint outputterraform output mysql_endpoint -
Open the public IP of
tutorial-web-lbin a browser. Use this output to openhttp://{WEB_LOAD_BALANCER_PUBLIC_IP}and verify that the Library page appears. Also test database-dependent operations such as registration and login to check the application and MySQL connections, as well as the web page.Check web service addressterraform output -raw web_load_balancer_public_ip
Step 5. Inspect instances (optional)
To access web and application instances directly, configure TCP port forwarding on the bastion host.
-
Check the Nginx Proxy Manager URL with these commands and open it in a browser. Access the bastion admin UI from an IP range specified in
admin_cidr.Check bastion IP and NPM URLterraform output bastion_public_ip
terraform output nginx_proxy_manager_url- Initial email:
admin@example.com - Initial password:
changeme
InformationChange the password immediately after logging in with the initial account.
- Initial email:
-
On the Streams tab, assign a port from
10000to10010for each instance you want to access. Add a rule forwarding it to the instance's private IP and port22. Check private IPs with these commands.Check web and application private IPsterraform output web_instance_private_ips
terraform output app_instance_private_ips -
After adding the rule, connect with the private key of the registered key pair. Replace
{PRIVATE_KEY_PATH},{FORWARDED_PORT}, and{BASTION_PUBLIC_IP}with the key file path, forwarding port, and bastion public IP.Connect through port forwardingchmod 400 "{PRIVATE_KEY_PATH}"
ssh -i "{PRIVATE_KEY_PATH}" -p "{FORWARDED_PORT}" ubuntu@{BASTION_PUBLIC_IP} -
On the instance, check
cloud-initcompletion and container state.Check initialization and containerssudo cloud-init status --wait
sudo docker ps -aThe web server should run
kakaocloud-library-client, and the application server should runkakaocloud-library-server. If installation or execution fails, inspect/var/log/cloud-init-output.logand the corresponding container logs.
Step 6. Check for changes and delete resources
After verifying deployment, run terraform plan again in the same directory. If you have not changed the configuration, check that there are no additional changes. If differences appear, review them and do not immediately apply changes you did not intend.
terraform plan
After the lab, delete the resources created in the example directory. Review the deletion plan and enter yes at the approval prompt. If you need the MySQL data, save it separately before deletion.
terraform destroy
After Terraform completes deletion, check terraform state list and the console for remaining resources. If deletion fails, check the connections and resource states mentioned in the error and run terraform destroy again. The existing key pair, which was only looked up, is not deleted.
Delete the resources after completing the lab. Managed MySQL and load balancers continue to incur charges while running, so delete them promptly after the lab.
Troubleshooting
Authentication or lookup errors
See authentication checks in the VM tutorial. This example also requires permissions to look up, create, and delete VPC, Load Balancer, and MySQL resources. Compare the key pair name, image, and instance type with the lookup conditions in keypair.tf, data.tf, and locals.tf and with the values available in the target project.
Web service is unavailable
Check connections in this order:
- Check the web load balancer's public IP, HTTP
80listener, and web instance target states. - Follow Inspect instances to check
cloud-initand container logs on web and application servers. - Check the internal NLB's TCP
8080listener and application instance target states. - Check the MySQL endpoint, account and password passed to the app, and allowed ports in each tier's security groups.
Some resources may already exist even if execution fails. Keep the state file, fix the cause, and review the plan again, or clean up with terraform destroy.