Introduction
AWS CodeDeploy is a service for automating application deployment, it's possible to use this service to deploy Lambda functions, ECS Services or EC2 Instances, here I describe only the last option.
CodeDeploy is responsible only for uploading the application bundle and running scripts listed in appspec.yml
, all resources, environment variables, logging tools
and process management need to be provisioned separately.
This approach offers a great flexibility but the whole process becomes complex. Because of that this deployment option may be considered only if other ones don't provide enough features.
I created a sample Terraform module that tries to replicate AWS Beanstalk features, it's available HERE, feel free to use it.
I describe here resources that Beanstalk provisions and all of those should be created if you want to imitate this setup.
Beanstalk Resources
Beanstalk Environment is a collection of AWS resources:
- AWS::AutoScaling::AutoScalingGroup (aws_autoscaling_group)
- AWS::AutoScaling::LaunchConfiguration (aws_launch_configuration)
- AWS::AutoScaling::ScalingPolicy (aws_autoscaling_policy)
- AWS::CloudWatch::Alarm (aws_cloudwatch_metric_alarm)
- AWS::EC2::SecurityGroup (aws_security_group)
If the property LoadBalancerType
from aws:elasticbeanstalk:environment
namespace is set to classic
Beanstalk creates following resource:
AWS::ElasticLoadBalancing::LoadBalancer (aws_elb)
for application
and network
values of LoadBalancerType
the Environment contains such resources:
- AWS::ElasticLoadBalancingV2::LoadBalancer (aws_lb)
- AWS::ElasticLoadBalancingV2::Listener (aws_lb_listener)
- AWS::ElasticLoadBalancingV2::TargetGroup (aws_lb_target_group)
For security reasons Beanstalk creates two Security Groups: one for a load balancer and one for EC2 instances.
Configuration of these security groups:
Security Group | Ingress | Egress |
Load Balancer | TCP port 80 src: 0.0.0.0/0 | TCP port 80 dest: 0.0.0.0/0 |
EC2 Instances | TCP port 80 src: Load Balancer SG | All traffic dest: 0.0.0.0/0 |
So the Security Group for EC2 Instances allows inbound traffic only from the Load Balancer Security Group,
it's a good practice that everyone should follow. Load Balancer Egress could have also been restricted
to the proper VPC CIDR block.
In Beanstalk Environment the Load Balancer proxies the traffic to the instance listening on port 80
,
in the presented solution nginx can use any port higher than 1024
so the security group can be parametrized
based on port that nginx listens on.
Also, it's worth to mention that AWS recommends to use AWS::EC2::LaunchTemplate instead of AWS::AutoScaling::LaunchConfiguration, because it provides the newest EC2 features, Launch Templates can also be versioned, there is no need to recreate the resource when update is required, on the other hand Launch Configurations need to be recreated every property update.
My AutoScaling Group has health_check_type
set to ELB
so every instance that will not pass the Load Balancer
health check will be terminated, the Load Balancer will monitor only instances that run some application version,
I describe it further. For the heath check target I use HTTP
protocol and the same port that nginx
uses (for me, it's 8080).
AutoScaling Group Instances Refresh
Even though aws_autoscaling_group uses the latest version of the aws_launch_template:
launch_template {
id = aws_launch_template.app-launch-template.id
version = aws_launch_template.app-launch-template.latest_version
}
every update of the template WILL NOT affect running EC2 instances, AutoScaling Group applies it only to new instances, it will not replace the existing ones. For instances replacement AWS introduced instance refresh, instances can be refreshed using StartInstanceRefresh (start_instance_refresh) action, the replacement process can be monitored using AWS Console or with DescribeInstanceRefreshes (describe_instance_refreshes) action.
Terraform is able to trigger instance refresh when the launch template has been changed, but it WILL NOT wait until the process is finished. When the infrastructure update is finished the deployment pipeline should detect if there is any pending instance refresh and wait until it's finished before starting the deployment.
Instance refresh configuration of an aws_autoscaling_group resource:
instance_refresh {
strategy = "Rolling"
preferences {
min_healthy_percentage = 99
}
}
Instances can be replaced using Rolling
option, so there will be no downtime.
When the process is triggered by Terraform you can wait it to finish by using AWS SDK,
describe_instance_refreshes
can return the status for every instance being replaced.
AutoScaling Group Load Balancer
It's important to not attach a Load Balancer to an AutoScaling Group using load_balancers
or target_group_arns
properties of aws_autoscaling_group,
because the Load Balancer should monitor the instances AFTER the first deployment, otherwise
it would terminate instances that don't have any version deployed yet. The Load Balancer is configured
for an aws_codedeploy_deployment_group,
and it will be attached after the successful deployment.
Components
In this section I present some important parts of my environment, including AMI, logging tools, EC2 security configuration, environment variables provisioning and other tools that are required to create a solution stack similar to one created by Beanstalk.
Environment AMI
The image can be provisioned by Packer and should consist of all tools
required by a specific environment (JRE for Java, Python interpreter, some native libraries)
together with nginx that
will be responsible for buffering incoming connections. You should provision every tool that
your chosen language stack requires. It's important to configure and install as many tools
as possible in this state, because it will speed up the scale-up
process, the newly provisioned
instances during high-load will need to download only the application bundle.
An example install script for a Python3
environment can be found HERE.
Of course the image should provide CodeDeploy Agent
that is responsible for unpacking the application bundle and perform deployment actions described in appspec.yml
file.
AWS provides different S3 buckets with CodeDeploy binary for every region so the current region can be fetched during AMI provisioning process:
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
INSTANCE_IDENTITY=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -v http://169.254.169.254/latest/dynamic/instance-identity/document)
REGION=$(jq -r '.region' <<< "$INSTANCE_IDENTITY")
CODE_DEPLOY_URL="https://aws-codedeploy-$REGION.s3.$REGION.amazonaws.com/latest/install"
wget "$CODE_DEPLOY_URL"
IAM and Security Groups
EC2 Security Group needs to allow HTTPS
(tcp
port 443
) outbound traffic to let the CodeDeploy Agent
download the application bundle from S3. There is no need for any additional inbound traffic rule (except the one for Load Balancer of course).
Security Group for EC2 Instances:
resource "aws_security_group" "instance-security-group" {
vpc_id = var.vpc_id
ingress {
security_groups = [aws_security_group.lb-security-group.id]
from_port = var.nginx-port
to_port = var.nginx-port
protocol = "tcp"
}
egress {
cidr_blocks = ["0.0.0.0/0"]
from_port = 443
to_port = 443
protocol = "tcp"
}
}
The IAM Role Policy for the instance is:
data "aws_iam_policy_document" "ec2-policy" {
version = "2012-10-17"
statement {
sid = "AllowDownloadBundle"
effect = "Allow"
actions = [
"s3:List*",
"s3:Get*"
]
resources = ["arn:aws:s3:::${local.app-name}-artifacts-${local.region}-${local.account_id}/*"]
}
statement {
sid = "AllowCloudWatchLogs"
effect = "Allow"
actions = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:TagLogGroup",
"logs:DescribeLogGroups",
"logs:DescribeLogStreams"
]
resources = [
"arn:aws:logs:${local.region}:${local.account_id}:log-group:/${local.app-name}/*",
"arn:aws:logs:${local.region}:${local.account_id}:log-group:/${local.app-name}/*:log-stream:*"
]
}
}
s3:List*
and s3:Get*
are listed HERE as required.
These actions are restricted only to the S3 bucket with application artifacts.
appspec.yml
appspec.yml
is a file required by CodeDeploy Agent to copy proper files and run lifecycle hooks.
The file should be provisioned as part of the application bundle. It's the core of the CodeDeploy
deployment process as it describes every action that is required to install the application.
Source of the appspec.yml
:
version: 0.0
os: linux
files:
- source: systemd/app.service
destination: /etc/systemd/system
- source: src
destination: /home/app
- source: nginx.conf
destination: /etc/nginx
- source: requirements.txt
destination: /home/app
- source: fluent-bit/app.conf
destination: /etc/fluent-bit/fluent-bit.conf.d
- source: systemd/journald.conf
destination: /etc/systemd
- source: web
destination: /home/proxy/web
file_exists_behavior: OVERWRITE
hooks:
BeforeInstall:
- location: scripts/stop_services.sh
- location: scripts/setup_env_var.sh
AfterInstall:
- location: scripts/init_venv.sh
- location: scripts/restart_journald.sh
- location: scripts/start_services.sh
ValidateService:
- location: scripts/test_proxy.sh
I use CodeDeploy to copy all required files to the destination folders, it can be used to copy config files, source code, compiled binaries or web pages.
Because the application, nginx and fluent-bit are managed by systemd they should be stopped first, then enabled or restarted when the configuration is changed, it can be done by lifecycle hooks. Hooks can also be used to verify application state, script that exits with error code fails the deployment.
Source of scripts/test_proxy.sh
#!/bin/bash
RETRIES=10
while ((RETRIES > 0)); do
RESPONSE=$(curl --write-out '%{http_code}' --silent --output /dev/null http://localhost:8080/health)
if [ "$RESPONSE" = "200" ]; then
exit 0
fi
RETRIES=$((RETRIES - 1))
sleep 2
done
exit 1
CodeDeploy hooks are not only restricted to Bash scripts. It can be for example a Python script:
#!/usr/bin/python3
print("Hello")
But it needs to start with a #!
followed by a path to the interpreter.
Systemd
Same as in the original Beanstalk environment systemd can be used as an application process supervisor, handle failures, run the application after EC2 instance restart and send logs to systemd-journald.
For every environment the service file may look quite different,
the basic configuration may consist of ExecStart
property and Type=simple
but it all depends on requirements.
Here is the sample app.service
file for a Python3
environment:
[Unit]
Wants=network-online.target
After=network-online.target cloud-final.service
[Service]
User=app
Group=app
Type=simple
ExecStart=/home/app/env/bin/gunicorn --bind :5000 --workers 3 --threads 2 app:app --log-level debug
RuntimeDirectory=gunicorn
WorkingDirectory=/home/app
Restart=on-failure
StandardOutput=journal
StandardError=journal
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true
EnvironmentFile=/etc/app/*.env
[Install]
WantedBy=cloud-init.target
This file is deployed as part of the application bundle and copied into /etc/systemd/system
by
the CodeDeploy Agent as defined in appspec.yml
.
Fluent-Bit
As CloudWatch Agent isn't able to collect logs from systemd-journald other logs collector should be used, I used fluent-bit because it's quite popular, the binary is very small, and it's able to fetch logs from journald and send to CloudWatch, it also offers basic logs processing with Filters and Streams.
Fluent-Bit configuration can be deployed as part of the application bundle, the fluent-bit.service
needs
to be restarted by the AfterInstall
hook script to apply newly copied configuration.
Fluent-Bit can also monitor the environment BEFORE the first deployment, the /etc/fluent-bit.conf
file
can be provided during an image creation, if the file contains such line:
@INCLUDE fluent-bit.conf.d/*.conf
The additional configuration can be fetched from /etc/fluent-bit.conf.d
directory, these configuration
can be deployed using CodeDeploy. to apply it fluent-bit.service
restart is required.
Source of /etc/fluent-bit.conf
:
[SERVICE]
Flush 1
Log_Level info
Parsers_File parsers.conf
[INPUT]
Name tail
Tag codedeploy-agent-${INSTANCE_ID}
path /var/log/aws/codedeploy-agent/codedeploy-agent.log
[OUTPUT]
Name cloudwatch_logs
Match codedeploy-agent-*
region ${REGION}
log_stream_prefix logs-
log_group_name /${APP_NAME}/codedeploy-agent-log-group
auto_create_group On
[INPUT]
Name systemd
Tag fluent-bit-${INSTANCE_ID}
Systemd_Filter _SYSTEMD_UNIT=fluent-bit.service
[OUTPUT]
Name cloudwatch_logs
Match fluent-bit-*
region ${REGION}
log_stream_prefix logs-
log_group_name /${APP_NAME}/fluent-bit-log-group
auto_create_group On
@INCLUDE fluent-bit.conf.d/*.conf
Source of /etc/fluent-bit.conf.d/app.conf
that is provisioned by CodeDeploy:
[INPUT]
Name systemd
Tag app-${INSTANCE_ID}
Systemd_Filter _SYSTEMD_UNIT=app.service
[OUTPUT]
Name cloudwatch_logs
Match app-*
region ${REGION}
log_stream_prefix logs-
log_group_name /${APP_NAME}/app-log-group
auto_create_group On
[INPUT]
Name systemd
Tag nginx-${INSTANCE_ID}
Systemd_Filter _SYSTEMD_UNIT=nginx.service
[OUTPUT]
Name cloudwatch_logs
Match nginx-*
region ${REGION}
log_stream_prefix logs-
log_group_name /${APP_NAME}/nginx-log-group
auto_create_group On
Environment variables INSTANCE_ID
, APP_NAME
and REGION
are provided by a fluent-bit-init-sh
script started
by a fluent-bit-init.service
:
[Unit]
Wants=network-online.target
Before=fluent-bit.service
After=network-online.target
[Service]
Type=oneshot
ExecStart=/opt/fluent-bit-init.sh
[Install]
WantedBy=fluent-bit.service
This service needs to be started Before
the fluent-bit.service
to generate /etc/fluent-bit/variables.env
environment file. Unfortunately it's not possible to fetch the variables by a script started with
ExecStartPre
of a fluent-bit.service
because systemd requires the EnvironmentFile
to exist
before the command is started. The Type
property is set to oneshot
to be sure, that fluent-bit.service
defined as dependent will not be started until the fluent-bit-init.service
is finished.
Part of install.sh
script started by Packer:
cp /tmp/fluent-bit-init.service /usr/lib/systemd/system
cp /tmp/fluent-bit-init.sh /opt
chmod +x /opt/fluent-bit-init.sh
mkdir -p /usr/lib/systemd/system/fluent-bit.service.d
cat <<EOT > /usr/lib/systemd/system/fluent-bit.service.d/00_env.conf
[Service]
EnvironmentFile=/etc/fluent-bit/variables.env
EOT
systemctl enable fluent-bit
systemctl enable fluent-bit-init
Source of fluent-bit-init.sh
:
#!/bin/bash
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
INSTANCE_IDENTITY=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -v http://169.254.169.254/latest/dynamic/instance-identity/document)
REGION=$(jq -r '.region' <<< "$INSTANCE_IDENTITY")
INSTANCE_ID=$(jq -r '.instanceId' <<< "$INSTANCE_IDENTITY")
APP_NAME=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -v http://169.254.169.254/latest/meta-data/tags/instance/Name)
cat <<EOT > /etc/fluent-bit/variables.env
REGION=${REGION}
INSTANCE_ID=${INSTANCE_ID}
APP_NAME=${APP_NAME}
EOT
IMPORTANT NOTE: This script fetches the application name from instance tags, this feature is disabled by default, to have an access to instance tags using meta-data URL the following configuration should be added to the aws_launch_template:
metadata_options {
http_endpoint = "enabled"
http_tokens = "required"
http_protocol_ipv6 = "disabled"
http_put_response_hop_limit = 1
instance_metadata_tags = "enabled"
}
The fluent-bit-init.sh
and fluent-bit-init.service
are provisioned by Packer:
build {
sources = ["source.amazon-ebs.main"]
provisioner "file" {
source = "../fluent-bit/fluent-bit.conf"
destination = "/tmp/fluent-bit.conf"
}
provisioner "file" {
source = "fluent-bit-init.service"
destination = "/tmp/fluent-bit-init.service"
}
provisioner "file" {
source = "fluent-bit-init.sh"
destination = "/tmp/fluent-bit-init.sh"
}
provisioner "shell" {
script = "install.sh"
execute_command = "echo 'packer' | sudo -S sh -c '{{ .Vars }} {{ .Path }}'"
}
}
Journald configuration
As mention in the previous part of these series the default journald
config allows only 1000 log messages within 30 seconds,
by using CodeDeploy the entire journald.conf
can be provisioned as part of the application bundle.
Example jorunald.conf
that will be copied into /etc/systemd
during the deployment process:
[Journal]
RateLimitInterval=30s
RateLimitBurst=10000
Then it can be restarted by a simple script configured as AfterInstall
CodeDeploy hook
Source of scripts/restart_journald.sh
:
#!/bin/bash
systemctl restart systemd-journald
Nginx
AWS Beanstalk uses Nginx as a proxy that
forwards data to the application. It's recommended to use such proxy for connection buffering, it can also
serve static content for the application. Nginx installed on Beanstalk instances binds to port 80
,
I think, that it's not required as binding to the first 1024 ports requires special capabilities.
Instead, I created a new user called proxy
that doesn't have any permissions and bind Nginx to port 8080
,
AWS Load Balancers can forward data to any port, so using Well Known Ports is not required.
My example nginx configuration is available HERE. It proxies traffic to the application and serves static content. The proxy also sets the same headers as Beanstalk:
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
In this setup the application needs to listen on port 5000
.
Environment Variables
Environment Variables are very important part of the deployment infrastructure as they are the easiest
way to provide URLs to Database, SQS Queues, S3 Bucket or some other services used by the application.
Because of that it would be nice to be able to provide environment variables as a parameter of the code-deploy
Terraform module:
module "code-deploy" {
source = "./code_deploy"
app-subnets = [local.first_subnet, local.second_subnet]
elb-subnets = [local.first_subnet, local.second_subnet]
environment_variables = {
S3_BUCKET_ARN = aws_s3_bucket.demo-app-data-bucket.arn,
QUEUE_URL = aws_sqs_queue.task-queue.url,
AWS_DEFAULT_REGION = data.aws_region.current.name
}
vpc_id = data.aws_vpc.default-vpc.id
app-name = "demo-app"
app-image-name = "demo-app-image"
app-health-path = "/health"
max-instances = 1
min-instances = 1
deployment-type = "ALL_AT_ONCE"
instances-update-policy = "ONE_AT_A_TIME"
role-id = aws_iam_role.app-role.id
allow-ssh = true
}
To achieve this I use EC2 UserData (which I usually use for running init script, but not this time),
it can be then fetched during the deployment process with BeformInstall
hook:
#!/bin/bash
mkdir -p /etc/app
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
USER_DATA=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -v http://169.254.169.254/latest/user-data)
echo "$USER_DATA" > /etc/app/00_infra_variables.env
The UserData can be fetched by the instance using the special IP address, then the fetched environment variables are saved into configuration file that is used by systemd to provide them to the application:
Part of the /etc/systemd/system/app.service
[Service]
EnvironmentFile=/etc/app/*.env
Of course when the UserData is used to provision environment variables there is no option to run the init script, everything needs to be provisioned during AMI creation process.
When the EnvironmentFile
path is set the systemd requires it to be in format:
VAR1=VALUE1
VAR2=VALUE2
To convert provided Terraform map into the properly formatted UserData
the following code can be used:
Part of the aws_launch_template
resource definition:
user_data = base64encode(join("\n", [for key, value in var.environment_variables : "${key}=${value}"]))
CodeDeploy resources
The main resource that bound every other in CodeDeploy is the aws_codedeploy_app:
resource "aws_codedeploy_app" "app" {
compute_platform = "Server"
name = var.app-name
}
It generally configures the application name and compute_platform
that for EC2 should be set to Server
.
The aws_codedeploy_deployment_group
configures everything related to the instances. There the AutoScaling Group is attached with
autoscaling_groups
field. Also, this is the right place to configure Load Balancer
with load_balancer_info
config and target_group_info
or elb_info
depending on the Load Balancer type
that has been used. By providing it here and NOT in the AutoScaling Group you can be
sure that EC2 Instances will be attached to the Load Balancer only after the deployment,
so the AutoScaling Group will not terminate instances marked as Unhealthy by the Load Balancer because
the initial version has not been deployed yet.
NOTE: As mentioned HERE CodeDeploy integrates with configured AutoScaling Group and every time a new instance is added because of Scale-Up action the current application version is deployed on this instance.
With aws_codedeploy_deployment_config you can configure deployment policies similar to ones defined by Beanstalk, like: ALL_AT_ONCE or ROLLING:
resource "aws_codedeploy_deployment_config" "app-deployment-config" {
deployment_config_name = "${var.app-name}-deployment-config"
minimum_healthy_hosts {
type = "HOST_COUNT"
value = var.deployment-type == "ALL_AT_ONCE" ? 0 : var.minimum-healthy-hosts
}
}
The entire procedure is described HERE,
it contains some good examples how this configuration affects the entire process
(Even if the value
is set to 0
at least one instance must succeed for the overall deployment to succeed).
Worker Environment
Besides of a typical Web Stack with a Load Balancer in front of the application Beanstalk offers Worker Environment that creates the following resources:
The queue is used to provide tasks for a Worker Application, every EC2 instance managed by AWS Beanstalk has sqsd installed, a serviced that performs the SQS queue polling and redirects fetched messages to an application with a proper HTTP endpoint configured.
It's also possible to run tasks periodically using cron expressions by
providing cron.yaml
file inside application bundle. These CRON mechanism also triggers configured HTTP endpoints.
To be honest I don't see any point of using Worker Environment, typical microservice with an HTTP interface also can
perform background tasks fetched from an SQS queue by provisioning a separated Thread Pool,
there is no need to separate such logic into another deployment stack. Even if the application is designed
only to perform some tasks fetched from an SQS it still needs to use a Web Framework to create HTTP endpoints
that sqsd
may trigger, and if so it can be also deployed a Web Environment with one /health
endpoint that
a Load Balancer may test. Also, any REST API can be easily added to such service, because the infrastructure
is already provided.
The CRON functionality provided by Beanstalk sends periodic tasks using SQS queue, these functionality can be provided using EventBridge (or CloudWatch Events) that can send notifications to SQS:
resource "aws_sqs_queue" "task-queue" {
visibility_timeout_seconds = 60
}
data "aws_iam_policy_document" "sqs-allow-events" {
statement {
effect = "Allow"
actions = ["sqs:SendMessage"]
principals {
identifiers = ["events.amazonaws.com"]
type = "Service"
}
resources = ["*"]
}
}
resource "aws_sqs_queue_policy" "allow-events" {
policy = data.aws_iam_policy_document.sqs-allow-events.json
queue_url = aws_sqs_queue.task-queue.url
}
resource "aws_cloudwatch_event_rule" "schedule-rule" {
schedule_expression = "rate(1 minute)"
}
resource "aws_cloudwatch_event_target" "sqs-event-target" {
arn = aws_sqs_queue.task-queue.arn
rule =aws_cloudwatch_event_rule.schedule-rule.name
}
Blue/Green Deployment
AWS CodeDeploy offers Blue/Green deployment option for those who prefer to have an immutable deployment. It can provision a new AutoScaling Group for every deployment or discover the existing one. Terraform documentation for the aws_codedeploy_deployment_group suggest using DISCOVER_EXISTING policy, because otherwise Terraform will not be able to manage the newly created AutoScaling Group. The downside of this approach is you pay for IDLE resources.
The Blue/Green deployment can be configured by adding the following config to the aws_codedeploy_deployment_group
resource:
autoscaling_groups = [aws_autoscaling_group.app-group[0].id]
blue_green_deployment_config {
green_fleet_provisioning_option {
action = "DISCOVER_EXISTING"
}
deployment_ready_option {
action_on_timeout = "CONTINUE_DEPLOYMENT"
}
terminate_blue_instances_on_deployment_success {
action = "KEEP_ALIVE" // TERMINATING removes ASG, not instances
}
}
lifecycle {
ignore_changes = [autoscaling_groups]
}
This configuration is fascinating, first question you may ask: "If DISCOVER_EXISTING is configured why there is only
one ASG provided?", BECAUSE THE OTHER ONE IS PROVIDED WITH create_deployment
API CALL, the targetInstances
map should contain a list autoScalingGroups
and this is the GREEN deployment group. So the BLUE group is initially configured with autoscaling_groups
property and the GREEN group needs to be passed as an argument
to the API call that triggeres the deployment. That's really confusing.
Also, the autoscaling_groups
field needs to be ignored, because CodeDeploy UPDATES
this field after the deployment, the AutoScaling Group that has just been provided using the API call is used to set this property internally, so Terraform will try to change it to the original one during next run.
There is no possibility to omit the autoscaling_groups
field, when it's omitted the deployment fails (so the field is not required but if missing then every deployment fails).
So if you expected CodeDeploy to manage the BLUE/GREEN deployment state internally and choose the proper AutoScaling Group for every triggered deployment you may be disappointed, you need to do it by yourself, fetch all configured groups, check which one is currently used and use the other one during the next deployment. It can be done easily with a script, but it should be provided by the CodeDeploy service itself.
The other thing is terminate_blue_instances_on_deployment_success
, you may want to terminate the old instances,
but it needs to be done with an API call, setting action
to TERMINATE
will remove the AutoScaling Group
created by Terraform. AutoScaling Group will start some new instances after termination, but CodeDeploy
will not deploy any version on these, so the instances will just run without any application.
Conclusion
The presented setup is complicated and requires some AWS knowledge, it should be used only if the environment needs to be fully customized. Also, because of its complexity it should be well documented to let the newcomers understand it, that is the main benefit of the AWS Beanstalk, it can be considered as a framework that usually works fine, some minor issues with Beanstalk can be solved by workarounds that I presented in the previous part.
There is also a third option: If you use docker for a local development (everyone does these days) you may consider using ECS, if you create a docker image with your application anyway (for example using docker-maven-plugin) it sounds like a natural choice. ECS provides multiple features for creating a microservice environment, but the ecs agent can be also used to provision a single docker application. There is nothing wrong with having an ECS Cluster with only one small EC2 instance running a single container.