Welcome to the third part of the series of posts where I will guide you through the steps for creating a modern web application in Python and deploying it to the cloud using Elastic Beanstalk. At the end of this article, you will have the infrastructure put into Terraform code.

Pre-requisites:

  • Basic Python knowledge.
  • Flask Python application deployed to AWS Elastic Beanstalk.
  • GitHub Actions workflow set up to deploy the app to AWS Elastic Beanstalk.

All posts in this series

Let’s jump right into it!

Installing Terraform

You need Terraform to put your infrastructure into code. Why do you want this? As we’re creating a reliable production-grade application, we are going to have a lot of different things in our infrastructure. The complexity of making a change will grow exponentially if we continue to use GUI to create all our resources.

We only created an application that prints “Hello World,” but we already have Elastic Beanstalk environment, Elastic Beanstalk application, S3 bucket. Fortunately, Beanstalk encapsulates a lot of resources, but chances are you will be creating a database, security groups, etc. And then, imagine you want to create a staging environment to test things before pushing to production. Recreating all those things would be a nightmare. Let’s avoid it and install Terraform.

Please follow the terraform tutorial. In the end, you should be able to successfully execute terraform -help in the terminal.

I’m using Terraform v0.12.28

Initializing Terraform

Now, let’s create a folder in our project named terraform.

mkdir terraform
cd terraform

Before we can proceed, let’s create an AWS user for terraform. If you already had an AWS account before starting this tutorial and you have AWS credentials configured locally – you can use your existing configuration. For others, let’s create a user called terraform.

As you may remember, we’ve already created a user for GitHub Actions in Part 2: Automated deployment to AWS Elastic Beanstalk using Github Actions. You just need to repeat those steps, except for this user, we need admin-level permissions since it will be used by Terraform to manage our infrastructure.

On the last screen, you will see the Access key ID and Secret access key. We now need to configure our local machine to store these credentials, so Terraform can use them. Let’s create a file ~/.aws/credentials and add the following code into it:

[default]
aws_access_key_id = <your access key id>
aws_secret_access_key = <your secret access key>

If you already have a default profile, you can just append this code to the file with credentials, but instead of default, use a different name. If you do so, then for each Terraform command in the terminal, you’ll have to add AWS_PROFILE=your_non_default_profile in the beginning of the command. There are other ways for specifying the profile for Terraform to use in the documentation. However, only the one I described really worked for me at the time of writing this article.

In the terraform folder of our project create the file called provider.tf and put the following code inside:

provider "aws" {
  region = "us-east-1"
}

Our next step is to create an S3 bucket where Terraform will store its state. You can read more about Terraform state here.

Let’s go to AWS console and create an S3 bucket with the name terraform-state-andrey(it has to be unique across all bucket names in AWS, so I put my name in here). We’ve already created an S3 bucket once in the previous article, so you can use it for reference. Keep all default settings.

Now add another file into our terrafrom folder called backend.tf and put this inside:

terraform {
  backend "s3" {
    bucket = "terraform-state-andrey"
    key    = "core/terraform.tfstate"
    region = "us-east-1"
  }
}

Now please run the command terraform init in the terminal (while inside the terraform folder). You should see something like this in your output:

Initializing the backend...

Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.

...bla-bla-bla

* provider.aws: version = "~> 2.70"

Terraform has been successfully initialized!

...bla-bla-bla

Now let’s get to the real stuff.

Elastic Beanstalk in Terraform

Please create a file elastic_beanstalk.tf and put this code inside:

resource "aws_elastic_beanstalk_application" "application" {
  name        = "my-awesome-app"
}

resource "aws_elastic_beanstalk_environment" "environment" {
  name                = "my-awesome-environment"
  application         = aws_elastic_beanstalk_application.application.name
  solution_stack_name = "64bit Amazon Linux 2 v3.0.3 running Python 3.7"
}

Congratulations, you’ve just described a piece of infrastructure as a code. The code should be pretty self-explanatory. These are the same things you’ve created in the AWS Console web interface in the first article. solution_stack_name is the name of an environment that Elastic Beanstalk will set up on servers it manages. We’ve just specified that we want an environment with Python 3.7. At the time I’m writing this, the latest solution_stack_name for Python application is "64bit Amazon Linux 2 v3.0.3 running Python 3.7", you may want to specify a more recent version if you’re reading this in the future. Here is the list of platform versions.

Now in the terminal, please execute terraform plan. Don’t worry, this command is not going to change anything in AWS. What you should get is this:

Terraform will perform the following actions:

  # aws_elastic_beanstalk_application.application will be created
  + resource "aws_elastic_beanstalk_application" "application" {
      + arn  = (known after apply)
      + id   = (known after apply)
      + name = "my-awesome-app"
    }

  # aws_elastic_beanstalk_environment.environment will be created
  + resource "aws_elastic_beanstalk_environment" "environment" {
      + all_settings           = (known after apply)
      + application            = "my-awesome-app"
      + arn                    = (known after apply)
      + autoscaling_groups     = (known after apply)
      + cname                  = (known after apply)
      + cname_prefix           = (known after apply)
      + endpoint_url           = (known after apply)
      + id                     = (known after apply)
      + instances              = (known after apply)
      + launch_configurations  = (known after apply)
      + load_balancers         = (known after apply)
      + name                   = "my-awesome-environmen"
      + platform_arn           = (known after apply)
      + queues                 = (known after apply)
      + solution_stack_name    = "64bit Amazon Linux 2 v3.0.3 running Python 3.7"
      + tier                   = "WebServer"
      + triggers               = (known after apply)
      + version_label          = (known after apply)
      + wait_for_ready_timeout = "20m"
    }

Plan: 2 to add, 0 to change, 0 to destroy.

Terraform tells us that according to the infrastructure, we defined in elastic_beanstalk.tf there are two resources in AWS that are missing. Terraform will create them for us after you execute terraform apply. Please do that. You would also need to enter yes in the terminal to approve the action.

Terraform creates resources in your AWS account. Please carefully review resources each time before you apply changes as some resources may cost you money. So far in this tutorial, we’re creating only resources that are within FREE tier, if you created your AWS account as a part of this tutorial.

So after we applied our changes… oh wait, I’ve got an error.

You must specify an Instance Profile for your EC2 instance in this region.

Under the hood, Elastic Beanstalk manages EC2 instances for us. So to create our infrastructure, we also need to obey its rules and specify which profile will be used for our instances. To do this, please modify the code inside elastic_beanstalk.tf so it looks like this:

resource "aws_elastic_beanstalk_application" "application" {
  name        = "my-awesome-app"
}

resource "aws_elastic_beanstalk_environment" "environment" {
  name                = "my-awesome-environment"
  application         = aws_elastic_beanstalk_application.application.name
  solution_stack_name = "64bit Amazon Linux 2 v3.0.3 running Python 3.7"

  setting {
        namespace = "aws:autoscaling:launchconfiguration"
        name      = "IamInstanceProfile"
        value     = "aws-elasticbeanstalk-ec2-role"
      }
}

We’ve just added one setting. aws-elasticbeanstalk-ec2-role is the name of the default instance profile that AWS provides. It has default permissions for you to get started (more about it here). We will be able to create our own profile in the future with Terraform.

After you do terraform apply again, hopefully, you see this:

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Let’s see it for ourselves in AWS Console. Go to Elastic Beanstalk service page.

Wow, now we have 2 Elastic Beanstalk environments! That’s too much! The first one on the list is the new one. If you click the URL you’ll see the default application that Elastic Beanstalk uses. But we want our own application, right?

Deploying to the new environment created in Terraform

Let’s change our GitHub Actions workflow to use the new environment. We need to make changes only to this section:

- name: Create new ElasticBeanstalk Application Version
        run: |
          aws elasticbeanstalk create-application-version \
          --application-name MyAwesomeApp \
          --source-bundle S3Bucket="my-awesome-app-deploy-andrey",S3Key="deploy_package.zip" \
          --version-label "ver-${{ github.sha }}" \
          --description "commit-sha-${{ github.sha }}"

      - name: Deploy new ElasticBeanstalk Application Version
        run: aws elasticbeanstalk update-environment --environment-name Myawesomeapp-env-1 --version-label "ver-${{ github.sha }}"
  1. MyAwesomeApp becomes my-awesome-app.
  2. Myawesomeapp-env-1 becomes my-awesome-environment.

If you have different names, don’t worry. Just change values from the environment we created by hand to values that come from the environment we created using Terraform.

We are almost ready to deploy our application. Before committing changes to GitHub, please add this to .gitignore, so we don’t push stuff generated by Terraform we don’t need:

.terraform/

Just to be sure our deploy works, let’s update the text we show on our only page in the Flask application. Change 'Hello GitHub Actions World!' into 'Hello Terraform World!'. OK, we’re ready. Push your changes to master. Remember that in the previous article, we set up our deployment pipeline to be triggered on each push to master. You just do that, sit and relax! It will take a few minutes.

Hopefully, GitHub Action finished successfully. Now please go to the newly created Elastic Beanstalk environment and verify that the status is OK.

If you follow the link in the top left corner, you’ll see this:

Awesome! Now please go to the AWS console and delete the Elastic Beanstalk environment we created by hand. We don’t need it anymore. We have the new one in Terraform now. And the beauty is, you can mess up everything in AWS, run terraform apply, and it will always know exactly what is wrong. Our infrastructure is not super complicated, so you may not get the benefit of Terraform right away. But imagine tens or hundreds of resources. You don’t want to manage that by hand and hope to keep everything in your head.

If you have any issues, please shoot me an email, and I’ll try to help you. The full code is available here.

Part 4 of this series is in progress.

You can read more of our blog here.

Categories: How to

1 Comment

How to deploy a Python Flask app to AWS Elastic Beanstalk · July 30, 2020 at 10:32 am

[…] Part 3: AWS Elastic Beanstalk infrastructure in code with Terraform. […]

Comments are closed.