Running an ASP.NET Core site on an EC2 Instance

I've been working on the Porter Photos project for a bit now, running it locally on my Mac mini. The only wrinkle with getting that working was that I installed the arm64 version of the .NET SDK alongside the x64 version and that was causing problems. I could run the site from the command line just fine but when Visual Studio Code tried to run it, it just didn't. No helpful errors at all. I eventually edited /etc/dotnet/install_location to point to the x64 install and that solved it.

My next challenge was getting the site running in a test environment, on an EC2 instance. First I created a build artifact to deploy to my test instance. This was easily accomplished.

dotnet publish --output BuildArtifacts --sc --os linux
cd BuildArtifacts
tar -acf PorterPhotos.tgz --exclude="PorterPhotos.tgz" *
aws s3 cp PorterPhotos.tgz {s3-URI-to-deploy-bucket-and-object-name} --no-progress

I wanted to publish as a single file, so I also added the following to my .csproj file.

<PropertyGroup>
  <PublishSingleFile>true</PublishSingleFile>
</PropertyGroup>

Next, I created an EC2 instance through the AWS Console and sshed to it. I installed the .NET runtime, downloaded my build artifact, configured Apache and a service to start Kestrel. I validated that this all worked and then wrote a shell script to do those steps.

ec2-cloud-init.sh:

#!/bin/bash
export PORTERPHOTOS_DEPLOY_S3_URI={s3-URI-to-deploy-bucket-and-object-name}
yum -y install httpd2.4 mod_ssl mod_rewrite mod_headers
wget https://dot.net/v1/dotnet-install.sh
chmod u+x ./dotnet-install.sh
./dotnet-install.sh -c 6.0 --runtime aspnetcore
ln -s /root/.dotnet/dotnet /usr/local/bin/dotnet
CAT << EOF >> .bash_profile
export PATH="$PATH:/root/.dotnet"
EOF
source .bash_profile
cat << EOF >> /etc/httpd/conf.d/porterphotos.conf
<VirtualHost *:*>
    RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
</VirtualHost>
<VirtualHost *:80>
    ProxyPreserveHost On
    ProxyPass / http://127.0.0.1:5000/
    ProxyPassReverse / http://127.0.0.1:5000/
#    ServerName www.example.com
#    ServerAlias *.example.com
    ErrorLog ${APACHE_LOG_DIR}porterphotos-error.log
    CustomLog ${APACHE_LOG_DIR}porterphotos-access.log common
</VirtualHost>
EOF
systemctl restart httpd
systemctl enable httpd
mkdir /var/www/porterphotos
aws s3 cp $PORTERPHOTOS_DEPLOY_S3_URI PorterPhotos.tgz --no-progress
tar -xvf PorterPhotos.tgz -C /var/www/porterphotos --no-same-owner
cat << EOF >> /etc/systemd/system/kestrel-porterphotos.service
[Unit]
Description=Porter Photos ASP.NET website (Kestrel)

[Service]
WorkingDirectory=/var/www/porterphotos
# ExecStart=/usr/local/bin/dotnet /var/www/porterphotos/PorterPhotos.dll
ExecStart=/var/www/porterphotos/PorterPhotos
Restart=always
# Restart service after 10 seconds if the dotnet service crashes:
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=dotnet-porterphotos
User=apache
Environment=ASPNETCORE_ENVIRONMENT=Production 

[Install]
WantedBy=multi-user.target
EOF
systemctl enable kestrel-porterphotos.service 
systemctl start kestrel-porterphotos.service

I then used the AWS CLI to create the instance and then wrote a wrapper-script to make that easy to do. The end result is that I just have to run a single command to start a test instance.

ec2-run-instances.sh:

#!/bin/bash
# not shown: a bunch of stuff to pull in some configuration settings
aws ec2 run-instances \
  --image-id ami-0ed9277fb7eb570c9 \
  --instance-type $INSTANCE_TYPE \
  --key-name $KEY_NAME \
  --user-data file://$CLOUD_INIT_PATH \
  --iam-instance-profile Name="$IAM_INSTANCE_PROFILE_NAME" \
  --security-group-ids $SECURITY_GROUP_IDS \
  --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$TAG_NAME}]" "ResourceType=volume,Tags=[{Key=Name,Value=$TAG_NAME}]"

I paired that with another script to terminate these instances...

ec2-terminate-instances.sh:

#!/bin/bash
# not shown: a bunch of stuff to pull in some configuration settings
# this script can be dangerous; remove --dry-run to do it for real
aws ec2 describe-instances \
  --query 'Reservations[*].Instances[*].{Instance:InstanceId,State:State.Name}' \
  --filters "Name=tag-value,Values=$TAG_NAME" \
  --output text | \
  grep -v terminated | \
  awk '{print $1}' | \
  while read line; do aws ec2 terminate-instances --instance-ids $line --dry-run; done

And now I can fire up and tear down test instances with a single command. How convenient.

(I plan to eventually deploy using CloudFormation. I haven't quite got that far yet.)

Jan 2nd, 2022