Create a CLI Tool to Manage Apache Virtual Hosts with Golang

Updated on November 21, 2023
Create a CLI Tool to Manage Apache Virtual Hosts with Golang header image

Introduction

The Golang programming language is suitable for creating fast and reliable cross-platform command-line interfaces. Typically, you code these types of scripts/packages to solve a particular problem in your Linux server. For instance, if you want to run multiple websites with Apache on your single server, the process of manually configuring the virtual hosts is tedious and time-consuming. A Golang CLI tool can automate the process for you.

When you code and compile CLI tools with Golang, you reap the benefits of fast binaries that are easier to distribute to end-users. In addition, the Golang flag library allows you to use and parse command-line arguments to customize how your final tools work.

In this guide, you'll create an elegant CLI tool with Golang to automate the process of creating virtual hosts with Apache on your Ubuntu 20.04 server.

Prerequisites

To complete this guide, you require:

1. The Process of Creating Virtual Hosts Manually

When you want to host multiple websites with Apache on a single server, you set up directories for each website, assign the correct file permissions, and set up a new virtual host configuration file. For instance, here is a detailed manual process that you can use to add the domain example.com to your server.

  1. Make a directory for the example.com website and set the correct permissions for the public_html sub-directory.

     $ sudo mkdir -p /var/www/example.com/public_html
     $ sudo chmod -R 755 /var/www/example.com/public_html
  2. Take ownership of the new /var/www/example.com/public_html directory.

     $ sudo chown -R $USER:$USER /var/www/example.com/public_html
  3. Create a virtual host file under the /etc/apache2/sites-available/ directory.

     $ sudo nano /etc/apache2/sites-available/example.com.conf

    Paste the information below into the example.com.conf file.

     <VirtualHost *:80>
    
       ServerAdmin admin@example.com
       ServerName example.com
       DocumentRoot /var/www/example.com/public_html
    
       <Directory /var/www/example.com/public_html>
         Options -Indexes +FollowSymLinks -MultiViews
         AllowOverride All              
         Require all granted
       </Directory>
    
       ErrorLog ${APACHE_LOG_DIR}/error.log
       CustomLog ${APACHE_LOG_DIR}/access.log combined
    
     </VirtualHost>
  4. Save and close the file.

  5. Disable the default virtual host file.

     $ sudo a2dissite 000-default.conf
  6. Enable the new example.com.conf virtual host file.

     $ sudo a2ensite example.com.conf
  7. Restart Apache to load the new virtual host.

    $ sudo systemctl restart apache2

As you can see, the above manual process is quite long and sometimes prone to errors if you make a typing mistake when configuring the directories and the configuration files. Also, only a seasoned Linux administrator can understand and follow the process.

In the next step, you'll create a Golang CLI tool that allows you to add a virtual host to the Apache web server by executing a single command with the domain name as an argument.

2. Creating a Virtual Host CLI Tool with Golang

SSH to your server to complete the following steps.

  1. Create a project directory under your home directory.

     $ mkdir project
  2. Switch to the new project directory.

     $ cd project
  3. Open a new vhs-automator.go file. You can name your CLI tool any name but for now, stick to the name vhs-automator.go to follow this guide.

     $ nano vhs-automator.go
  4. Initialize the vhs-automator.go file and import all the libraries you will use. The flag library is the main candidate here, and it allows you to accept and parse command-line flags and arguments.

     package main
    
     import (
         "flag"            
         "fmt" 
         "os"
         "os/exec"
         "os/user"

    "strconv" "io/ioutil" )

  5. Next, add the main() function and initialize a domain variable using the flag.String() method. If an end-user doesn't enter a value for the domain, the script should exit with an error code.

     func main() {
    
         domain := flag.String("domain", "", "Enter the virtual host domain name.(Required)")
         flag.Parse()
    
         if *domain == "" {
             flag.PrintDefaults()
             os.Exit(1)
         }
  6. Next, use the os library and the MkdirAll method to create a directory for the domain under the /var/www directory. Here, you're retrieving the domain name from the command line as entered by the user using the syntax *domain. The root user owns the directories you're creating here.

         fmt.Println("Creating directory and permissions for the new domain. \n")
    
         err := os.MkdirAll("/var/www/" + *domain + "/public_html", 0755)     
    
         if err != nil {                
             fmt.Println(err)
             os.Exit(1)
         } else {
             fmt.Println("Directory and permissions for the " + *domain + " created successfully. \n")
         }
  7. Next, use the statement user.Lookup(os.Getenv("SUDO_USER") to get the current User ID and Group ID of the user running the command. Then, use the statement os.Chown to change the ownership of the public_html to the current user.

         usr, err := user.Lookup(os.Getenv("SUDO_USER"))
    
         if err != nil {
            fmt.Println(err)
            os.Exit(1)
         }
    
         Uid, err := strconv.ParseInt(usr.Uid, 10, 64)
         user_id := int(Uid)
    
         Gid, err := strconv.ParseInt(usr.Gid, 10, 64)
         group_id := int(Gid)
    
         fmt.Println("Setting file ownership for the new domain. \n") 
    
         err = os.Chown("/var/www/" + *domain + "/public_html", user_id, group_id)
    
         if err != nil {
             fmt.Println(err)
             os.Exit(1)
         } else {
             fmt.Println("File owernship for the new domain set successfully. \n")
         }
  8. Next, create a virtual host file with the parameters of the new domain name using the Golang ioutil library.

         vh_settings := `<VirtualHost *:80>` + "\n\n" +
    
                        `  ServerAdmin admin@` + *domain + "\n" +
                        `  ServerName ` + *domain + "\n" +
                        `  DocumentRoot /var/www/` + *domain + `/public_html` + "\n\n" +
    
                        `  <Directory /var/www/` + *domain + `/public_html>` + "\n" +
                        `     Options -Indexes +FollowSymLinks -MultiViews` + "\n" +
                        `     AllowOverride All ` + "\n" +              
                        `     Require all granted` + "\n" +
                        `   </Directory>` + "\n\n" +
    
                        `   ErrorLog ${APACHE_LOG_DIR}/error.log` + "\n" +
                        `   CustomLog ${APACHE_LOG_DIR}/access.log combined` + "\n\n" +
    
                        ` </VirtualHost>`
    
    
         conf_data := []byte(vh_settings)
    
         fmt.Println("Creating virtual host configuration file... \n")
    
         err = ioutil.WriteFile("/etc/apache2/sites-available/" + *domain + ".conf", conf_data, 0644)
    
         if err != nil {      
             fmt.Println(err)
             os.Exit(1)
         } else {
            fmt.Println("Virtual host configuration file created. \n")
         }
  9. Use the exec.Command to enable the new virtual host configuration file and restart the apache web server.

         fmt.Println("Enabling " + *domain + ".conf file...\n")  
    
         cmd := exec.Command("a2ensite", *domain + ".conf")
         stdout, err := cmd.Output()
    
         if err != nil {
             fmt.Println(err.Error())
             os.Exit(1)
         } else {
             fmt.Print(string(stdout))
             fmt.Println(*domain + ".conf configuration file enabled successfully. \n")  
         }
    
         fmt.Println("Restarting Apache...\n")  
    
         cmd = exec.Command("systemctl", "restart", "apache2")
         stdout, err = cmd.Output()
    
         if err != nil {
             fmt.Println(err.Error())
             os.Exit(1)
         } else {
             fmt.Print(string(stdout))
             fmt.Println("Apache server restarted successfully. \n")  
         }
     }
  10. After adding all the code blocks, your vhs-automator.go file should be similar to:

    package main
    
    import (
        "flag"            
        "fmt" 
        "os"
        "os/exec"
        "os/user"

    "strconv" "io/ioutil" )

    func main() {
    
        domain := flag.String("domain", "", "Enter the virtual host domain name.(Required)")
        flag.Parse()
    
        if *domain == "" {
            flag.PrintDefaults()
            os.Exit(1)
        }
    
        fmt.Println("Creating directory and permissions for the new domain. \n")
    
        err := os.MkdirAll("/var/www/" + *domain + "/public_html", 0755)     
    
        if err != nil {                
            fmt.Println(err)
            os.Exit(1)
        } else {
            fmt.Println("Directory and permissions for the " + *domain + " created successfully. \n")
        }            
    
        usr, err := user.Lookup(os.Getenv("SUDO_USER"))
    
        if err != nil {
           fmt.Println(err)
           os.Exit(1)
        }
    
        Uid, err := strconv.ParseInt(usr.Uid, 10, 64)
        user_id := int(Uid)
    
        Gid, err := strconv.ParseInt(usr.Gid, 10, 64)
        group_id := int(Gid)
    
        fmt.Println("Setting file ownership for the new domain. \n") 
    
        err = os.Chown("/var/www/" + *domain + "/public_html", user_id, group_id)
    
        if err != nil {
            fmt.Println(err)
            os.Exit(1)
        } else {
            fmt.Println("File owernship for the new domain set successfully. \n")
        }
    
        vh_settings := `<VirtualHost *:80>` + "\n\n" +
    
                       `  ServerAdmin admin@` + *domain + "\n" +
                       `  ServerName ` + *domain + "\n" +
                       `  DocumentRoot /var/www/` + *domain + `/public_html` + "\n\n" +
    
                       `  <Directory /var/www/` + *domain + `/public_html>` + "\n" +
                       `     Options -Indexes +FollowSymLinks -MultiViews` + "\n" +
                       `     AllowOverride All ` + "\n" +              
                       `     Require all granted` + "\n" +
                       `   </Directory>` + "\n\n" +
    
                       `   ErrorLog ${APACHE_LOG_DIR}/error.log` + "\n" +
                       `   CustomLog ${APACHE_LOG_DIR}/access.log combined` + "\n\n" +
    
                       ` </VirtualHost>`
    
    
        conf_data := []byte(vh_settings)
    
        fmt.Println("Creating virtual host configuration file... \n")
    
        err = ioutil.WriteFile("/etc/apache2/sites-available/" + *domain + ".conf", conf_data, 0644)
    
        if err != nil {      
            fmt.Println(err)
            os.Exit(1)
        } else {
           fmt.Println("Virtual host configuration file created. \n")
        }
    
        fmt.Println("Enabling " + *domain + ".conf file...\n")  
    
        cmd := exec.Command("a2ensite", *domain + ".conf")
        stdout, err := cmd.Output()
    
        if err != nil {
            fmt.Println(err.Error())
            os.Exit(1)
        } else {
            fmt.Print(string(stdout))
            fmt.Println(*domain + ".conf configuration file enabled successfully. \n")  
        }
    
        fmt.Println("Restarting Apache...\n")  
    
        cmd = exec.Command("systemctl", "restart", "apache2")
        stdout, err = cmd.Output()
    
        if err != nil {
            fmt.Println(err.Error())
            os.Exit(1)
        } else {
            fmt.Print(string(stdout))
            fmt.Println("Apache server restarted successfully. \n")  
        }
    }
  11. Save and close the file when you're through with editing.

3. Testing and Compiling the vhs-automator Tool

Up to this point, you can now use your vhs-automator.go script to add new virtual domains on your server.

  1. Make sure the default virtual host is disabled. This is the only command that you've to run manually since you should do it once throughout the lifetime of your server.

     $ sudo a2dissite 000-default.conf
  2. Then, add the 3 sample domains by executing the commands below. Your domain names should follow the -domain flag.

     $ sudo go run vhs-automator.go -domain example.com
     $ sudo go run vhs-automator.go -domain example.net
     $ sudo go run vhs-automator.go -domain example.org
  3. To confirm whether your tool is working as expected, you may create sample content under the public_html directory of each domain by running the command below.

     $ echo 'The example.com is working' | sudo tee /var/www/example.com/public_html/index.html
     $ echo 'The example.net is working' | sudo tee /var/www/example.net/public_html/index.html
     $ echo 'The example.org is working' | sudo tee /var/www/example.org/public_html/index.html
  4. If you've pointed the domain names to your server's public IP address, you can now visit the URLs below to check if everything is working as expected. Also, you can edit your hosts' file (That is C:\Windows\System32\drivers\etc\hosts in windows and etc/hosts in Linux) and point the domain names above to your server's public IP address to test if the configurations are working as expected.

    Sample outputs from the browser.

     The example.com is working
     The example.net is working
     The example.org is working
  5. Build the vhs-automator.go package.

     $ sudo go build vhs-automator.go
  6. You should now have a binary with the following name in your current project directory.

     vhs-automator
  7. Add the binary to the /usr/local/bin directory. This allows you to execute the binary just by entering its name in the Linux shell.

     $ sudo cp vhs-automator /usr/local/bin/vhs-automator
  8. Execute the vhs-automator binary. This time around, create another domain. For instance, anydomain.example.

     $ sudo vhs-automator -domain anydomain.example
  9. You should receive the following response as your anydomain.example is created on the server.

     Creating directory and permissions for the new domain.
    
     Directory and permissions for the anydomain.example created successfully.
    
     Setting file ownership for the new domain.
    
     File owernship for the new domain set successfully.
    
     Creating virtual host configuration file...
    
     Virtual host configuration file created.
    
     Enabling anydomain.example.conf file...
    
     Enabling site anydomain.example.
     To activate the new configuration, you need to run:
      systemctl reload apache2
     anydomain.example.conf configuration file enabled successfully.
    
     Restarting Apache...
    
     Apache server restarted successfully.
  10. Create sample content for the new website.

     $ echo 'The anydomain.example is working' | sudo tee /var/www/anydomain.example/public_html/index.html
  11. If you now visit the domain http://anydomain.example on a browser, you should get the following output.

     The anydomain.example is working

Your Golang script is now working as expected.

Conclusion

In this guide, you've created a command-line interface tool with Golang to automate the process of adding Apache virtual hosts on your Ubuntu 20.04 server. Use the knowledge in this guide to create powerful CLI tools with Golang.