DevContainer and Makefile: A Duo for Simplified GCP Workflows
Tags: devops, docker, gcp, codenewbie
Published: 2023-08-27 16:12:45

Setting up a development environment can sometimes feel like orchestrating a symphony with disparate instruments, given how many moving parts are involved. However, DevContainers within Visual Studio Code (VSCode) are here to change that tune. Below, let's explore how to streamline this process and create a seamless, shareable, and consistent development environment.

## Step 1: Installing the DevContainer Extension in VSCode

Before diving into the code, ensure that the DevContainer extension is installed within your VSCode setup. This extension is your gateway to crafting isolated and reproducible development environments.
    

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bi5ssc5wosv3pa8j03fq.png)


## Step 2: Setting Up the Directory Structure
Create a new folder which will act as the root of your project.
Inside this folder, create another directory named .devcontainer.
Within .devcontainer, create two essential files: devcontainer.json and Dockerfile.
    

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kh5lqkoputg2zxc2r6m6.png)


## Step 3: Crafting the Dockerfile
The Dockerfile is like a recipe, dictating the ingredients and steps required to create your development environment. Below is an example using Ubuntu as the base image and installing Google Cloud SDK among other packages.

```bash
# Use an official Ubuntu as a parent image
FROM ubuntu:latest

# Set environment variables to non-interactive (this prevents some prompts)
ENV DEBIAN_FRONTEND=non-interactive

# Run package updates, install packages, and clean up
RUN apt-get update -y \
    # Install common packages and openssh-client
    && apt-get install -y \
        ca-certificates \
        curl \
        gnupg \
        openssh-client \
        python3 \
        make \
    # Add Google Cloud SDK repo and its key
    && echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] http://packages.cloud.google.com/apt cloud-sdk main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list \
    && curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key --keyring /usr/share/keyrings/cloud.google.gpg add - \
    && apt-get update \
    && apt-get install -y google-cloud-sdk \
    # Remove unnecessary files
    && rm -rf /var/lib/apt/lists/* \
    # Generate SSH Key
    && ssh-keygen -t rsa -b 4096 -f /root/.ssh/id_rsa -N ""

WORKDIR /automation
COPY . /automation/
```


## Step 4: Configuring devcontainer.json
This JSON file is the control center for the DevContainer. It specifies how the container should behave when we open VSCode inside it.

```bash
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
{
	"name": "Code space configuration",
	"build": {
		"dockerfile": "Dockerfile",
		// "args": { 
		// 	// Update 'VARIANT' to pick a .NET Core version: 2.1, 3.1, 5.0
		// 	"VARIANT": "5.0",
		// 	// Options
		// 	"INSTALL_NODE": "true",
		// 	"NODE_VERSION": "lts/*",
		// 	"INSTALL_AZURE_CLI": "true",
		// 	"INSTALL_TERRAFORM": "true",
		// 	"TERRAFORM_FILE":"terraform_1.0.4_linux_amd64.zip",
		// 	"TERRAFORM_VERSION":"https://releases.hashicorp.com/terraform/1.0.4/terraform_1.0.4_linux_amd64.zip"
		// }
	},

	// Set *default* container specific settings.json values on container create.
	"settings": {
    "terminal.integrated.shell.linux": "/bin/zsh"
  },

	// Add the IDs of extensions you want installed when the container is created.
	"extensions": [
		// "ms-dotnettools.csharp",
		// "hashicorp.terraform",
		// "ms-vscode.azure-account",
		// "ms-azuretools.vscode-azurefunctions",
		// "ms-azuretools.vscode-azureresourcegroups",
		// "github.copilot",
		// "ms-mssql.mssql",
		// "hookyqr.beautify",
		// "ms-azuretools.vscode-docker"
	],

	// Use 'forwardPorts' to make a list of ports inside the container available locally.
	// "forwardPorts": [5000, 5001],

	// Use 'postCreateCommand' to run commands after the container is created.
	"postCreateCommand": "uname -a",

	// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
	"remoteUser": "root"
}
```


## Step 5: Initiating the DevContainer
Run the Dev Containers: Reopen in Container command from VSCode. If everything is set up correctly, you'll land inside the container, where you can check the Google Cloud SDK version, among other things.

\[If you encounter issues, you can debug by running ``` docker run -it ubuntu:latest /bin/bash``` and manually inputting the commands from your Dockerfile for analysis.\]


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z9sl6rn89kct9lh6k4rb.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/52vfyrtzhxb3r9fh7iwc.png)


## Step 6: Automating Tasks with Makefile
You can further enhance your DevContainer experience by utilizing a Makefile. This file serves as a task runner that can execute a series of commands for you. Here's a sample that demonstrates printing a simple "hello":

```bash
greet := hello
echo:
	@ echo ${greet}
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wq7xrpuc83ja44aw7aex.png)


## Step 7: Integrating Google Cloud Platform
To start working with Google Cloud Platform (GCP), obtain service credentials and place them in a auth.json file. Then, update your Makefile to interact with GCP resources:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x3tcy15bday3x75mvy6a.png)


```bash
# Makefile for Google Cloud Platform (GCP)

# Variables
KEY_FILE := auth.json
PROJECT_ID := playground-s-11-b2eb9fb3
VPC_NAME := vpc-tr
SUBNET_NAME := subnet-tr
REGION := us-central1
COMPUTE_ZONE := us-central1-a
CIDR_BLOCK := 10.10.0.0/16
VM_NAME := vm-tr
VM_TYPE := n1-standard-1
VM_IMAGE_FAMILY := debian-11
VM_IMAGE_PROJECT := debian-cloud
INTERNAL_IP_RANGE := 31.43.23.23

SSH_KEY_PATH := /root/.ssh/id_rsa.pub
# Read the SSH public key into a variable
SSH_PUBLIC_KEY := root $(shell cat ${SSH_KEY_PATH})

# Default rule
default: gcloud_login gcloud_set_project gcloud_list_networks

# Rule for logging into GCP
gcloud_login:
	@ gcloud auth activate-service-account --key-file=${KEY_FILE}

# Rule for setting GCP project
gcloud_set_project:
	@ gcloud config set project ${PROJECT_ID}

# Rule for setting GCP compute zone
gcloud_set_zone:
	@ gcloud config set compute/zone ${COMPUTE_ZONE}

# Rule for listing GCP networks
gcloud_list_networks:
	@ gcloud compute networks list

# Rule for creating a custom VPC
create_vpc:
	@ gcloud compute networks create ${VPC_NAME} --subnet-mode=custom

# Rule for creating a custom VPC with subnets
create_vpc_with_subnets: create_vpc
	@ gcloud compute networks subnets create ${SUBNET_NAME} --network=${VPC_NAME} --region=${REGION} --range=${CIDR_BLOCK}

# Rule for deleting a VPC
delete_vpc:
	@ gcloud compute networks delete ${VPC_NAME} --quiet

# Rule for creating firewall rule for internal traffic
create_firewall_internal:
	@ gcloud compute firewall-rules create allow-internal --network ${VPC_NAME} --allow tcp,udp,icmp --source-ranges ${INTERNAL_IP_RANGE}

# Rule for creating firewall rule to allow all IP ranges
create_firewall_allow_all:
	@ gcloud compute firewall-rules create allow-all-${VPC_NAME} --network ${VPC_NAME} --allow tcp,udp,icmp --source-ranges 0.0.0.0/0

# Rule for creating firewall rule for SSH, RDP, and ICMP
create_firewall_ssh_rdp_icmp:
	@ echo "Creating firewall rules for VPC: ${VPC_NAME}"
	@ gcloud compute firewall-rules create allow-ssh-rdp-icmp-${VPC_NAME} --network ${VPC_NAME} --allow tcp:22,tcp:3389,icmp

# Rule for describing a specific GCP network
describe_network:
	@ gcloud compute networks describe ${VPC_NAME}

# Rule for listing subnets in the VPC
gcloud_list_subnets:
	@ gcloud compute networks subnets list --filter="network:${VPC_NAME}"

# Rule for creating a VM instance
gcloud_create_vm:
	@ gcloud compute instances create ${VM_NAME} \
        --zone=${COMPUTE_ZONE} \
        --machine-type=${VM_TYPE} \
        --image-family=${VM_IMAGE_FAMILY} \
        --image-project=${VM_IMAGE_PROJECT} \
        --boot-disk-size=10GB \
        --metadata=ssh-keys="$(SSH_PUBLIC_KEY)"

# Rule for deleting a VM instance
gcloud_delete_vm:
	@ gcloud compute instances delete ${VM_NAME} --zone=${COMPUTE_ZONE} --quiet

# Rule for deploying a GCP function
deploy_function:
	@ gcloud functions deploy FUNCTION_NAME --runtime RUNTIME --trigger-http --allow-unauthenticated

# Phony targets
.PHONY: gcloud_login gcloud_set_project gcloud_set_zone gcloud_list_networks create_vpc delete_vpc create_firewall_internal create_firewall_allow_all create_firewall_ssh_rdp_icmp describe_network create_vpc_with_subnets gcloud_list_subnets gcloud_create_vm gcloud_delete_vm deploy_function
```


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/24rcgd4yexf4e0wws0kd.png)

## Running Any File
You're not limited to specific languages or files. The DevContainer allows you to run any script, be it Shell, Python, or even a simple HTTP server.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ks9ojh7d4s5d49f2q9l8.png)



```bash
//hello.sh
echo "hello"

// Dockerfile
FROM nginx

//makefile
image_name:=tanvir/web
tag:=v1.1
container_name:=web
build:
	@ docker build -t ${image_name}:${tag} .
run:
	@ docker run --name ${container_name} -d -p 8080:80 ${image_name}:${tag}
b:
	@ chmod +x hello.sh 
	@ ./hello.sh

files:=hello.sh hello1.sh hello2.sh
chmod:
	@ chmod +x ${files}
run1:
	@ ./hello.sh
	@ ./hello1.sh
```

With that, you have a modular, version-controlled, and fully functional development environment. The best part is that all of this is contained within a DevContainer, ensuring that each team member can replicate the exact environment, thereby eliminating the "it works on my machine" syndrome. Happy coding!