Customizing our AWS Elastic Beanstalk environment with .ebextensions

Customizing our AWS Elastic Beanstalk environment with .ebextensions

Deploying a web application on AWS Elastic Beanstalk can sometimes require environment-specific customizations, such as configuring a database, modifying server settings, or installing additional software. Elastic Beanstalk extensions, or .ebextensions, are a set of configuration files used to customize and configure AWS Elastic Beanstalk environments. These files allow us easily tailor our environment to accommodate these requirements, ensuring a seamless deployment and optimal performance for your application.

To use .ebextensions, create a folder named .ebextensions in the root directory of our source bundle. Inside this folder, add one or more configuration files with a .config extension. These files should be written in YAML or JSON format and contain instructions to customize your AWS Elastic Beanstalk environment. When you deploy your application, Elastic Beanstalk will read and apply the configurations specified in these files.

The sections of those .config files can be grouped into three categories:

  • Options settings: Used to change any AWS Elastic Beanstalk configuration option.

  • Custom resources: Let us define additional AWS resources beyond the functionality provided by configuration options. You can add and configure any resources supported by AWS CloudFormation.

  • Software customization: Let us configure the EC2 instances. Every time a server is launched in your environment, AWS Elastic Beanstalk runs the operations defined to prepare the operating system.

In a previous post, we discussed How to Deploy a Windows Service on AWS Elastic Beanstalk using Terraform. Let's add a couple of requirements to our previous exercise:

  • Disable the operative system firewall.

  • Redirect HTTP traffic to HTTPS at the load balancer level.

Create a .ebextensions folder at the solution level, and within it, create a disable-firewall.config file with the following content:

files:
  "C:\\scripts\\firewall.ps1":
    content: |
      Set-NetFirewallProfile -Enabled False

commands:
  disable_firewall:
    command: powershell.exe -ExecutionPolicy Bypass -Command "C:\\scripts\\firewall.ps1"
    ignoreErrors: false
    waitAfterCompletion: 5

Files and Commands are known as Keys, and all of them are:

  • Packages: To download and install prepackaged applications and components.

  • Sources: To download an archive file from a public URL and unpack it in a target directory.

  • Files: To create files on the EC2 instance.

  • Commands: To execute commands on the EC2 instance.

  • Services: To define which services should be started or stopped when the instance is launched.

  • Container commands: To execute commands that affect your application source code. Container commands run after the application and web server have been set up and the application version archive has been extracted, but before the application version is deployed.

To redirect HTTP traffic to HTTPS, we need to modify our Terraform main.tf file to add the listener on port 433:

terraform {
  required_providers {
    aws = {
        source = "hashicorp/aws"
        version = "4.22.0"
    }
  }
}

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

data "aws_iam_policy_document" "assume_service_role" {
  statement {
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["elasticbeanstalk.amazonaws.com"]
    }

    actions = ["sts:AssumeRole"]

    condition {
        test     = "StringEquals"
        variable = "sts:ExternalId"

        values = [
        "elasticbeanstalk"
        ]
    }
  }
}

resource "aws_iam_role" "service_role" {
  name               = "aws-elasticbeanstalk-service-role"
  assume_role_policy = data.aws_iam_policy_document.assume_service_role.json
}

resource "aws_iam_role_policy_attachment" "AWSElasticBeanstalkEnhancedHealth-attach" {
  role       = aws_iam_role.service_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSElasticBeanstalkEnhancedHealth"
}

resource "aws_iam_role_policy_attachment" "AWSElasticBeanstalkManagedUpdatesCustomerRolePolicy-attach" {
  role       = aws_iam_role.service_role.name
  policy_arn = "arn:aws:iam::aws:policy/AWSElasticBeanstalkManagedUpdatesCustomerRolePolicy"
}

data "aws_iam_policy_document" "assume_role" {
  statement {
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }

    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role" "role" {
  name               = "aws-elasticbeanstalk-ec2-role"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

resource "aws_iam_role_policy_attachment" "CloudWatchFullAccess-attach" {
  role       = aws_iam_role.role.name
  policy_arn = "arn:aws:iam::aws:policy/CloudWatchFullAccess"
}

resource "aws_iam_role_policy_attachment" "AWSElasticBeanstalkWebTier-attach" {
  role       = aws_iam_role.role.name
  policy_arn = "arn:aws:iam::aws:policy/AWSElasticBeanstalkWebTier"
}

resource "aws_iam_role_policy_attachment" "AWSElasticBeanstalkWorkerTier-attach" {
  role       = aws_iam_role.role.name
  policy_arn = "arn:aws:iam::aws:policy/AWSElasticBeanstalkWorkerTier"
}

resource "aws_iam_role_policy_attachment" "AWSElasticBeanstalkMulticontainerDocker-attach" {
  role       = aws_iam_role.role.name
  policy_arn = "arn:aws:iam::aws:policy/AWSElasticBeanstalkMulticontainerDocker"
}

resource "aws_iam_instance_profile" "instance_profile" {
  name = "aws-elasticbeanstalk-ec2-role"
  role = aws_iam_role.role.name
}

resource "aws_s3_bucket" "bucket" {
  bucket = var.bucket
}

resource "aws_s3_object" "bucket_object" {
  bucket = aws_s3_bucket.bucket.id
  key    = "app-${uuid()}.zip"
  source = "app.zip"
}


resource "aws_elastic_beanstalk_application" "application" {
  name        = var.application
}

resource "aws_elastic_beanstalk_environment" "environment" {
  name                = "${var.application}-env"
  application         = aws_elastic_beanstalk_application.application.name
  solution_stack_name = var.platform
  tier                = "WebServer"

  setting {
    namespace = "aws:ec2:vpc"
    name      = "VPCId"
    value     = var.vpc_id
  }

  setting {
    namespace = "aws:ec2:vpc"
    name      = "Subnets"
    value     = var.ec2_subnets
  }

  setting {
    namespace = "aws:ec2:vpc"
    name      = "ELBScheme"
    value     = "internet facing"
  }

  setting {
    namespace = "aws:ec2:vpc"
    name      = "ELBSubnets"
    value     = var.elb_subnets
  }

  setting {
    namespace = "aws:autoscaling:launchconfiguration"
    name      = "IamInstanceProfile"
    value     =  aws_iam_role.role.name
  }

  setting {
    namespace = "aws:autoscaling:launchconfiguration"
    name      = "InstanceType"
    value     = var.instance_type
  }

  setting {
    namespace = "aws:autoscaling:launchconfiguration"
    name      = "EC2KeyName"
    value     = var.keypair
  }

  setting {
    namespace = "aws:elasticbeanstalk:environment"
    name      = "LoadBalancerType"
    value     = "application"
  }

  setting {
    namespace = "aws:elasticbeanstalk:environment"
    name      = "ServiceRole"
    value     = aws_iam_role.service_role.name
  }

  setting {
      namespace = "aws:elasticbeanstalk:environment:process:default"
      name      = "HealthCheckPath"
      value     = var.health_check_path
  }

  setting {
      namespace = "aws:elbv2:listener:443"
      name      = "Protocol"
      value     = "HTTPS"
  }

  setting {
      namespace = "aws:elbv2:listener:443"
      name      = "SSLCertificateArns"
      value     = var.ssl_certificate
  }

  setting {
      namespace = "aws:elbv2:listener:443"
      name      = "SSLPolicy"
      value     = "ELBSecurityPolicy-2016-08"
  }
}

resource "aws_elastic_beanstalk_application_version" "version" {
  bucket      = aws_s3_bucket.bucket.id
  key         = aws_s3_object.bucket_object.id
  application = aws_elastic_beanstalk_application.application.name
  name        = "${var.application}-app-${uuid()}"
}

Under .ebextensions folder, add an alb-http-to-https-redirection.config file as follows:

Resources:
  AWSEBV2LoadBalancerListener:
   Type: AWS::ElasticLoadBalancingV2::Listener
   Properties:
     LoadBalancerArn:
       Ref: AWSEBV2LoadBalancer
     Port: 80
     Protocol: HTTP
     DefaultActions:
       - Type: redirect
         RedirectConfig:
           Host: "#{host}"
           Path: "/#{path}"
           Port: "443"
           Protocol: "HTTPS"
           Query: "#{query}"
           StatusCode: "HTTP_301"

The resources that AWS Elastic Beanstalk creates for our environment can be found here. To create the bundle for deployment, execute the following commands:

dotnet publish ./src/WeatherApi/WeatherApi.csproj --output "terraform/publish/webapi" --configuration "Release" --framework "net6.0" /p:GenerateRuntimeConfigurationFiles=true --runtime win-x64 --no-self-contained

Compress-Archive -Path terraform/publish/webapi/* -DestinationPath terraform/bundle/site.zip

dotnet publish ./src/WeatherWs/WeatherWs.csproj --output "terraform/bundle/ws" --configuration "Release" --framework "net6.0" /p:GenerateRuntimeConfigurationFiles=true --runtime win-x64 --no-self-contained

copy .\install.ps1 .\terraform\bundle
copy .\aws-windows-deployment-manifest.json .\terraform\bundle

mkdir .\terraform\bundle\.ebextensions
copy .\.ebextensions\* .\terraform\bundle\.ebextensions

Compress-Archive -Path terraform/bundle/* -DestinationPath terraform/app.zip

The contents of the bundle will appear as follows:

|-- .ebextensions
|-- ws
|-- aws-windows-deployment-manifest.json
|-- install.ps1
`-- site.zip

Run the terraform scripts with the following commands:

cd terraform
terraform init
terraform plan -out app.tfplan -var="health_check_path=/swagger/index.html" -var="bucket=app-tf-001" -var="keypair=<MY_KEY_PAIR>" -var="instance_type=t2.medium" -var="application=app-tf-001" -var="vpc_id=<MY_VPC>" -var="ec2_subnets=<MY_SUBNETS>" -var="elb_subnets=<MY_SUBNETS>" -var="platform=64bit Windows Server 2019 v2.11.3 running IIS 10.0" -var ssl_certificate="<MY_SSL_CERTIFICATE>"
terraform apply 'app.tfplan'

And deploy the application version with the following command:

aws --region us-east-2 elasticbeanstalk update-environment --environment-name <OUTPUT_ENV_NAME> --version-label <OUTPUT_APP_VERSION>

In conclusion, AWS Elastic Beanstalk's .ebextensions provide a powerful and flexible way to customize your environment to meet specific requirements. By following the steps outlined in this article, you can easily configure various aspects of your application, such as disabling the operating system firewall and redirecting HTTP traffic to HTTPS. With a better understanding of .ebextensions, you can now further explore and leverage their capabilities to enhance your application's deployment and performance. You can find several config files under this repository. The code and scripts used in this article are available here. Thanks, and happy coding.