DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • Building Scalable Data Lake Using AWS
  • Building a Scalable ML Pipeline and API in AWS
  • Breaking AWS Lambda: Chaos Engineering for Serverless Devs
  • AWS Step Functions Local: Mocking Services, HTTP Endpoints Limitations

Trending

  • How to Introduce a New API Quickly Using Micronaut
  • Useful System Table Queries in Relational Databases
  • Is Big Data Dying?
  • How to Use AWS Aurora Database for a Retail Point of Sale (POS) Transaction System
  1. DZone
  2. Software Design and Architecture
  3. Security
  4. AWS LetsEncrypt Lambda or Why I Wrote a Custom TLS Provider for AWS Using OpenTofu and Go

AWS LetsEncrypt Lambda or Why I Wrote a Custom TLS Provider for AWS Using OpenTofu and Go

LetsEncrypt Lambda helps to manage TLS certificates. Compared to Certificate Manager it provides certs that can be used at non-only AWS services like EC2 Nginx.

By 
Alexander Sharov user avatar
Alexander Sharov
·
Oct. 02, 24 · Tutorial
Likes (3)
Comment
Save
Tweet
Share
6.9K Views

Join the DZone community and get the full member experience.

Join For Free

These days, it's challenging to imagine systems that have public API endpoints without TLS certificate protection. There are several ways to issue certificates:

  • Paid wildcard certificates that can be bought from any big TLS provider
  • Paid root certificates that sign all downstream certificates that are issued by corporate PKI systems
  • Free certificates issued by TLS providers like LetsEncrypt or AWS Certificate Manager
  • Self-signed certificates, issued by OpenSSL or another tool

    Let's Encrypt logo

Within the context of this post, I will mainly discuss free certificates that can be used inside of AWS, but not only by AWS services. Clearly, using anything other than AWS Certificate Manager makes no sense if you exclusively use managed AWS services and don't have strict security requirements. AWS Certificate Manager offers a very convenient and speedy method of issuing certificates via DNS or HTTP challenges; however, you face basic AWS limitations if you need to use these certificates outside of AWS services (API Gateway, ALB, NLB, etc.), such as an EC2 instance running Nginx that needs a physical certificate file. Additionally, even if you request it, AWS Certificate Manager does not display the certificate content.

At this point, it’s a good time to remind you about LetsEncrypt, a more widely used tool than Certificate Manager — at least because it doesn't depend on the cloud. Unfortunately, there are no built-in LetsEncrypt certificate issuance techniques available in AWS. It is possible to utilize the certbot tool for your EC2 or ECS services, but in that scenario, you will need to consider how to configure the renewal process. I also don't want to combine different strategies since I think it's better to have a single procedure for everything since it reduces the whole system's complexity.

Taking that into consideration, I created a Lambda function that automatically issues and renews LetsEncrypt certificates without requiring complex configuration. The certificate can be utilized at any AWS service using ARN along with AWS Certificate Manager certificates after the initial certificate issue. Additionally, you can use a physical certificate version that is kept in AWS Secrets Manager in whatever location you choose, whether it be an EC2 instance running Nginx or another place.

How AWS LetsEncrypt Lambda Works

  • Note: In this article, I'll assume that your DNS zone is managed by AWS Route53.

The Lambda function that is described in this article is written on Go v1.22. All outcome resources such as DNS records, secrets, or certificates are controlled by Amazon IAM role, which is created via Terraform code by default. The sequence of Lambda actions is the following:

  • Get an event containing a certificate list. Typically, this event can be a result of manual execution, or execution by cron that is made via aws_cloudwatch_event_target. Event example:
JSON
 
{
   "domainName": "hackernoon.referrs.me",
   "acmeUrl": "prod",
   "acmeEmail": "alexander.sharov@cloudexpress.app",
   "reImportThreshold": 10,
   "issueType": "default",
   "storeCertInSecretsManager" : true
}


  • Verify whether the certificate exists in the AWS Certificate Manager. If yes, confirm the expiration date.
  • Start the LetsEncrypt DNS-01 challenge if the number of days until the expiration date is fewer than the reImportThreshold. This step involves Lambda creating a TXT record matching the domain name to the AWS Route53 zone and waiting for your certificate to be ready.
  • Lambda updates the certificate in the AWS Certificate Manager when it's ready.
  • Lambda will store certificate files inside the AWS Secrets Manager if storeCertInSecretsManager is true.

AWS LetsEncrypt Lambda, sequence diagram

AWS LetsEncrypt Lambda, sequence diagram

Lambda Implementation Details

The Code

The Lambda is written on Go 1.22. Using as few libraries as possible helped me maintain my goal of keeping the code dry. The full list of required go libraries:

URL

Description

github.com/aws/aws-lambda-go

Libraries, samples, and tools to help Go developers develop AWS Lambda functions

github.com/aws/aws-sdk-go-v2

AWS SDK for the Go programming language.

github.com/go-acme/lego

LetsEncrypt / ACME client and library.

github.com/guregu/null

Reasonable handling of nullable values.

github.com/sirupsen/logrus

Structured, pluggable logging for Go.

Docker Image

I used gcr.io/distroless/static:nonroot as a basic docker image. For Go applications that don't require libc, this image is perfect. It is not completely empty as scratch and includes the following:

  • CA certificates: No need to copy them from any other stage
  • /etc/passwd: Contains users and groups such as nonroot
  • /tmp folder
  • tzdata: In case you want to set the timezone other than UTC

Build Process

In large software projects, overseeing the build process can turn into a laborious and time-consuming chore. Makefiles can help automate and streamline this process, ensuring that your project is built efficiently and consistently. For that reason, I prefer to use Makefile for all my Golang projects. The file is simple:

CMake
 
##@ General
help: ## Display this help.
	@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n  make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf "  \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

fmt: ## Run go fmt against code.
	go fmt ./...

vet: ## Run go vet against code.
	go vet ./...

##@ Build
build: fmt vet  ## Build service binary.
	go build -o bin/lambda main.go

run: vet ## Run service from your laptop.
	go run ./main.go

##@ Test
lint: ## Run Go linter
	golangci-lint run ./...

test: ## Run Go tests
	go test ./...


From the CI/CD side, I used the typical setup for the Go application:

  1. GitHub Actions as continuous integration
  2. ghcr.io as Docker registry: Compared to DockerHub, this one offers two key features that make it my preference to use:
    1. The build, test, and deployment workflows can be more easily automated straight from the GitHub repository thanks to GHCR's smooth integration with GitHub Actions. This can increase productivity and simplify the development process.
    2. GHCR leverages GitHub’s permission model, allowing users to manage access to container images using the same teams and permissions they use for their code repositories. This simplifies user management and enhances security.
  3. kvendingoldo/semver-action - My GitHub Actions plugin for automatic versioning: It’s a GitHub action that generates SemVer-compatible tags for repository commits. The action can manage versions, generate GitHub releases, and control release branches inside of the repository. It works wonderfully with both single and mono repos.
  4. Automatic changelog generation: I enjoy changelogs! In the context of other OpenSource projects that I manage (e.g., tofuutils/tenv, tofuutils/tofuenv, etc.), my team recognized the importance of informing users about changes.
  5. golangci-lint: From my perspective, all code should be reviewed by a static code analyzer. SonarQube cannot be set up for all projects, however golangci, in my opinion, is sufficient for small to medium Go projects.
  6. codespell.yml: In addition to code checking, it is a good idea to verify the grammar, especially if you have a large amount of documentation.

How To Deploy Lambda to AWS via Terraform/OpenTofu

  • Note: If you need to manage multiple versions of OpenTofu or Terraform, use the tenv - OpenTofu, Terraform, Terragrunt, and Atmos version manager, written in Go.
  • Note: More Terraform/OpenTofu examples can be found in the examples folder within the Git repository.

To work with AWS LetsEncrypt Lambda via OpenTofu, you need to do the following steps:

  • Add module to your OpenTofu/Terraform code:
JSON
 
module "letsencrypt_lambda" {
  source = "git@github.com:kvendingoldo/aws-letsencrypt-lambda.git//files/terraform/module?ref=0.31.4"

  blank_name = "kvendingoldo-letsencrypt-lambda"
  tags       = var.tags

  cron_schedule = var.letsencrypt_lambda_cron_schedule
  events        = var.letsencrypt_lambda_events

  ecr_proxy_username     = var.ecr_proxy_username
  ecr_proxy_access_token = var.ecr_proxy_access_token
}


  • Specify variables:
JSON
 
variable "tags" {
  default = {
    hackernoon : "demo"
  }
}

variable "ecr_proxy_username" {
  default = "kvendingoldo"
}

variable "ecr_proxy_access_token" {
  default = "ghp_xxx"
}

variable "letsencrypt_lambda_cron_schedule" {
  default = "rate(168 hours)"
}
variable "letsencrypt_lambda_events" {
  default = [
    {
      "acmRegion" : "us-east-1",
      "route53Region" : "us-east-1",
      "domainName" : "hackernoon.referrs.me",
      "acmeUrl" : "stage",
      "acmeEmail" : "alexander.sharov@cloudexpress.app",
      "reImportThreshold" : 100,
      "issueType" : "default",
      "storeCertInSecretsManager" : false
    }
  ]
}


  • Pay attention to variables ecr_proxy_username and ecr_proxy_access_token. By default, AWS Lambda cannot pull images from sources other than AWS ECR. Fortunately, the AWS team created ECR Proxy cache, which can fetch images from publicly available registries such as DockerHub or GHCR and store them inside ECR. Despite this possibility, AWS does not allow images to be pulled without a token, even from open public repositories; thus, you must acquire a personal GitHub token to gain access to pre-built Docker images. Alternatively, you can pull my GitHub repository, build the image locally, then upload it to your pre-existing ECR repository. In this scenario, the example can be modified as follows:
JSON
 
module "letsencrypt_lambda" {
  source = "../../"

  blank_name = "kvendingoldo-letsencrypt-lambda"
  tags       = var.tags

  cron_schedule = var.letsencrypt_lambda_cron_schedule
  events        = var.letsencrypt_lambda_events

  ecr_proxy_enabled = false
  ecr_image_uri     = "<YOUR_ACCOUNT_ID>.dkr.ecr.us-east-2.amazonaws.com/aws_letsencrypt_lambda:<VERSION>"
}


  • When you complete changing the code, run the following command to install OpenTofu by OpenTofu version switcher tenv: $ tenv tofu install. 
  • And finally, execute the following commands to apply the produced code:
Shell
 
$ tofu init
$ tofu plan
$ tofu apply


  • Wait until the code is deployed to AWS and events are triggered. After a few minutes, you will see ready certificates inside the certificate manager. Example:

A list of issues certificates with AWS LetsEncrypt Lambda inside of AWS Certificate Manager

A list of issues certificates with AWS LetsEncrypt Lambda inside of AWS Certificate Manager

  • Starting now, AWS can use the issued certificate at any services by ARN.
  • If you need to use the certificate outside of AWS services or have access to its content, set the storeCertInSecretsManager event option to true. In this situation, when Lambda completes the basic execution, the certificate will be saved in AWS Secrets Manager. It gives users more flexibility: they can inspect the certificate's content, work with it directly from EC2, etc. To learn more about AWS Secrets Manager, read the official guide.

Example of the issued certificate, that is stored inside of AWS Secrets Manager.

Example of the issued certificate, that is stored inside of AWS Secrets Manager

Environment Variables

Name

Description

Possible values

Default value

Example

Required

FORMATTER_TYPE

Formatter type for logs

JSON | TEXT

TEXT

JSON

❌

MODE

Application mode. Set cloud mode for AWS execution, and local mode for local testing.

cloud | local

cloud

cloud

✅

LOG_LEVEL

Logging level

panic|fatal|error|warn|info|debug|trace

warn

warn

❌

AWS_REGION

Default AWS Region. After the deployment to AWS it settings automatically.

<any valid AWS region>

-

us-east-1

✅

DOMAIN_NAME

Domain name for which the certificate is being issued or renewed

any valid domain name

-

hackernoon.referrs.me

✅

ACME_URL

The production LetsEncrypt URL will be utilized if it is set to prod; if not, the stage URL will be used.

prod | stage

prod

prod

✅

ACME_EMAIL

Email address linked to the LetsEncrypt certificate

any valid email

alexander.sharov@cloudexpress.app

alexander.sharov@cloudexpress.app

✅

REIMPORT_THRESHOLD

The certificate will be renewed if its time to live (TTL) equals REIMPORT_THRESHOLD.

any int > 0

10

10

✅

STORE_CERT_IN_SECRETSMANAGER

If true, Lambda will keep the certificate in both Certificate Manager and Secrets Manager.

true | false

“false”false

“false”

❌

How To Check LetsEncrypt Lambda Logs

In the scope of work with aws-letsencrypt-lambda, you may occasionally want to review the logs. It's quite easy to accomplish:

  1. Go to AWS Cloudwatch, and click on “Log groups.”
  2. Find log group with name, that you specified in OpenTofu code. For example, in my case it’s /aws/lambda/kvendingoldo-letsencrypt-lambda. 
  3. Go to the group, select the desired stream from the list, and review the logs.

How To Trigger Lambda Manually via AWS UI

  1. Go to Lambda function that has been created via OpenTofu. Click to “Tests” button.

    AWS LetsEncrypt Lambda: UI interface

    AWS LetsEncrypt Lambda: UI interface
  2. Fill Test Event and click Test: 

    JSON
     
    {
      "domainName": "<YOUR_VALID_DOMAIN>",
      "acmeUrl": <stage | prod>,
      "acmeEmail": "<ANY_VALID_EMAIL>",
      "reImportThreshold": 10,
      "issueType": "<default | force>",
      "storeCertInSecretsManager" : <true | false>
    }


    Example #1:

    JSON
     
    {
       "domainName": "hackernoon.referrs.me",
       "acmeUrl": "prod",
       "acmeEmail": "alexander.sharov@cloudexpress.app",
       "reImportThreshold": 10,
       "issueType": "default"
    }

  3. Wait until execution will be completed. You can the execution log is available in Cloudwatch. Usually the initial issue takes around 5 minutes.

How To Test Lambda Locally

  1. Clone the aws-letsencrypt-lambda repository to your laptop.
  2. Configure AWS Cli credentials via the official guide.
  3. Examine the environment variables section and set the minimum number of variables needed. Since LetsEncrypt will limit the amount of retries per hour for ACME_URL="prod", I advise using ACME_URL="stage" for testing. Environment variables example:

    Shell
     
    export AWS_REGION="us-east-2"
    export MODE=local
    export DOMAIN_NAME="hackernoon.referrs.me"
    export ACME_URL="stage"
    export ACME_EMAIL="alexander.sharov@cloudexpress.app"
    export REIMPORT_THRESHOLD=10
    export ISSUE_TYPE="default"
    export STORE_CERT_IN_SECRETSMANAGER="true"

  4. Execute the lambda locally via the following command:
    Shell
     
    $ go run main.go

  5. Following Lambda's successful execution, the following log will appear.
    Plain Text
     
    INFO[0000] Starting lambda execution ...                
    INFO[0000] Lambda will use STAGING ACME URL; If you need to use PROD URL specify it via 'ACME_URL' or pass in event body 
    INFO[0000] Certificate found, arn is arn:aws:acm:us-east-2:004867756392:certificate/72f872fd-e577-43f4-ae38-6833962630af. Trying to renew ... 
    INFO[0000] Checking certificate for domain 'hackernoon.referrs.me' with arn 'arn:aws:acm:us-east-2:004867756392:certificate/72f872fd-e577-43f4-ae38-6833962630af' 
    INFO[0000] Certificate status is 'ISSUED'               
    INFO[0000] Certificate in use by []                     
    INFO[0000] Certificate valid until 2024-08-31 13:50:49 +0000 UTC (89 days left) 
    INFO[0000] Try to get certificate for hackernoon.referrs.me domain 
    2024/06/02 17:56:23 [INFO] acme: Registering account for alex.sharov@referrs.me
    2024/06/02 17:56:24 [INFO] [hackernoon.referrs.me, www.hackernoon.referrs.me] acme: Obtaining bundled SAN certificate
    2024/06/02 17:56:25 [INFO] [hackernoon.referrs.me] AuthURL: https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/12603809394
    2024/06/02 17:56:25 [INFO] [www.hackernoon.referrs.me] AuthURL: https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/12603809404
    2024/06/02 17:56:25 [INFO] [hackernoon.referrs.me] acme: Could not find solver for: tls-alpn-01
    2024/06/02 17:56:25 [INFO] [hackernoon.referrs.me] acme: Could not find solver for: http-01
    2024/06/02 17:56:25 [INFO] [hackernoon.referrs.me] acme: use dns-01 solver
    2024/06/02 17:56:25 [INFO] [www.hackernoon.referrs.me] acme: Could not find solver for: tls-alpn-01
    2024/06/02 17:56:25 [INFO] [www.hackernoon.referrs.me] acme: Could not find solver for: http-01
    2024/06/02 17:56:25 [INFO] [www.hackernoon.referrs.me] acme: use dns-01 solver
    2024/06/02 17:56:25 [INFO] [hackernoon.referrs.me] acme: Preparing to solve DNS-01
    2024/06/02 17:56:26 [INFO] Wait for route53 [timeout: 5m0s, interval: 4s]
    2024/06/02 17:57:00 [INFO] [www.hackernoon.referrs.me] acme: Preparing to solve DNS-01
    2024/06/02 17:57:00 [INFO] Wait for route53 [timeout: 5m0s, interval: 4s]
    2024/06/02 17:57:30 [INFO] [hackernoon.referrs.me] acme: Trying to solve DNS-01
    2024/06/02 17:57:30 [INFO] [hackernoon.referrs.me] acme: Checking DNS record propagation. [nameservers=109.122.99.130:53,109.122.99.129:53]
    2024/06/02 17:57:34 [INFO] Wait for propagation [timeout: 5m0s, interval: 4s]
    2024/06/02 17:57:46 [INFO] [hackernoon.referrs.me] The server validated our request
    2024/06/02 17:57:46 [INFO] [www.hackernoon.referrs.me] acme: Trying to solve DNS-01
    2024/06/02 17:57:46 [INFO] [www.hackernoon.referrs.me] acme: Checking DNS record propagation. [nameservers=109.122.99.130:53,109.122.99.129:53]
    2024/06/02 17:57:50 [INFO] Wait for propagation [timeout: 5m0s, interval: 4s]
    2024/06/02 17:58:30 [INFO] [www.hackernoon.referrs.me] The server validated our request
    2024/06/02 17:58:30 [INFO] [hackernoon.referrs.me] acme: Cleaning DNS-01 challenge
    2024/06/02 17:58:30 [INFO] Wait for route53 [timeout: 5m0s, interval: 4s]
    2024/06/02 17:59:09 [INFO] [www.hackernoon.referrs.me] acme: Cleaning DNS-01 challenge
    2024/06/02 17:59:09 [INFO] Wait for route53 [timeout: 5m0s, interval: 4s]
    2024/06/02 17:59:43 [INFO] [hackernoon.referrs.me, www.hackernoon.referrs.me] acme: Validations succeeded; requesting certificates
    2024/06/02 17:59:43 [INFO] Wait for certificate [timeout: 30s, interval: 500ms]
    2024/06/02 17:59:45 [INFO] [hackernoon.referrs.me] Server responded with a certificate.
    INFO[0203] Certificate has been successfully imported. Arn is arn:aws:acm:us-east-2:004867756392:certificate/72f872fd-e577-43f4-ae38-6833962630af 
    INFO[0204] Secret updated successfully. SecretId: arn:aws:secretsmanager:us-east-2:004867756392:secret:hackernoon.referrs.me-NioT77 
    INFO[0204] Lambda has been completed

  6. That is it. Starting now, AWS can use the issued certificate at any services by ARN or in other locations where it is physically necessary by obtaining it from the AWS Secrets Manager.

Hands-On Experience Using It Over More Than 4 Years at AWS

I've been using the Lambda function in production for almost four years. Over the years, various aspects of the initial implementation have changed:

  1. Previously, AWS prohibited the usage of any non-ECR registries as Lambda sources. It has not changed; however, AWS has added an ECR proxy for GitHub, DockerHub, and a few additional registries. Without this functionality, we had to manually push Lambda images to our personal ECR and replace URL to the image in Terraform code. Now the OpenTofu code does it automatically via ECR Proxy.
  2. In the beginning, I considered introducing various challenges such as http-01 or tls-alpn-01, but no one questioned me about it for four years. It is still present on GitHub issues, and if this capability is required, we can work together to create it.
  3. I didn't want to utilize LetsEncrypt certificates at pure EC2 instances when the project originally started, but these days it's standard practice. As I previously stated, in certain situations, a certificate can be retrieved from AWS Secrets Managed using the AWS CLI.
  4. I’ve written a lot of new Go code over the years, so I can tell that the original Lambda code in my repository isn't as fancy as it could be. There is a significant difference between it and my most recent Go project, tenv (OpenTofu, Terraform, Terragrunt, and Atmos version manager, written in Go), but in any case, the code is still generally supported, so making modifications to it won't be too problematic. Occasionally, I will undertake significant refactoring to make the code more elegant.
  5. The same Lambda is being used for years in several different projects. Additionally, I'm co-founder of DevOps platform cloudexpress.app, where our team manages TLS certificates for all our clients using AWS LetsEncrypt Lambda to simplify the automation processes.

Now let's talks about numbers. Over a period of 4 years, this project has helped many people and been used in numerous OpenSource and over 30 commercial projects. The Lambda issues more than 2000 certificates and don’t want to stop on that.

Conclusion

AWS LetsEncrypt Lambda is a suitable solution for you, if:

  • You must have a physical version of the certificate and will use it from non-AWS native services such as EC2 Nginx.
  • You do not want to rely on AWS Certificate Manager to manage the TLS certificate issuance and renewal process (check logs, set renewal dates, etc.).
  • You would like to get email notifications from LetsEncrypt when your certificate expires, or will be expired soon.
  • You want to personalize the solution by changing the Golang code (for example, changing the LetsEncrypt challenge, storing the certificate in Hashicorp Vault, etc.).

If you discovered that at least one of these points applies to your situation, you are welcome to use AWS Lambda. Also, if you wish to participate in development, I am always open to new issues and Pull Requests at GitHub.

AWS AWS Lambda TLS Go (programming language)

Published at DZone with permission of Alexander Sharov. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Building Scalable Data Lake Using AWS
  • Building a Scalable ML Pipeline and API in AWS
  • Breaking AWS Lambda: Chaos Engineering for Serverless Devs
  • AWS Step Functions Local: Mocking Services, HTTP Endpoints Limitations

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!