以下は2020年の AWS re:Invent の開催概要です。
毎年大量のアップデートがアナウンスされ、情報を追うのに必死ですが、今回のエントリ内容にかかわるところでは以下がアップデートされました。
新しい AmazonEBS 汎用ボリューム gp3 のご紹介
また少し遡りますが、9月には以下のアップデートがありました。
AWS Graviton2 プロセッサを搭載した新しい Amazon EC2 T4g インスタンスを発表
ポイントを引用します。
T4g インスタンスを使用すると、T3 インスタンスと比較して 20% 低いコストで最大 40% のパフォーマンス上のメリットが得られます。これにより、幅広いワークロードに対して最高のコストパフォーマンスを実現できます。
ストレージキャパシティーに関係なくパフォーマンスをプロビジョニングでき、既存の gp2 ボリュームタイプよりも 20% 低い価格で提供される
コストを抑えてパフォーマンスを最適化できるかもしれない! ということでやってみました。このブログの構成は過去のエントリで紹介していますが、構成図を再掲しておきます。
以下はコンテナ構成の再掲です。
各コンテナの名前は以下のようにしています。
役割 | 名前 |
---|---|
Nginx コンテナ | django.webserver |
Django コンテナ | django.application |
PostgreSQL コンテナ | django.database |
今回変更する部分の新旧比較です。
# | AMI | InstanceType | VolumeType |
---|---|---|---|
旧 | amzn2-ami-hvm-2.0.20190618-x86_64-gp2 | t3.micro | gp2 |
新 | amzn2-ami-hvm-2.0.20201126.0-arm64-gp2 | t4g.micro | gp3 |
フォルダ構成です。
# フォルダ構成
minbase.io
├── build
│ ├── django
│ └── nginx
├── django
│ ├── blog
│ │ └── fixtures
│ ├── config
│ │ └── settings.py
│ ├── manage.py
│ ├── static
│ ├── templates
│ └── uwsgi.ini
├── docker-compose.yml
├── nginx
│ ├── conf.d
│ │ └── default.conf
│ ├── nginx.conf
│ ├── templates
│ │ └── default.conf.template
│ └── uwsgi_params
└── recovery
├── cron
├── data
└── image
リポジトリはなるべく最新化するよう心がけています。過去記事と細部が相違している可能性がありますので、ご注意ください。
今回の移行では AWS サービスとしては以下を利用しました。
AWS サービス | 用途 |
---|---|
Amazon S3 | 資材置場として利用。EC2 からアップロード / ダウンロードを行う |
AWS Systems Manager パラメータストア | EC2 セットアップの際に参照するパラメータをセキュアに保管する |
これらのサービスを利用するために、EC2 インスタンスに IAM ロールを付与する必要があります。まずは IAM ロールに登録する IAM ポリシーを作成します。パラメータストアに関しては、以下のような形にしました。実際はリソースの階層をもっと絞ったほうがよいとは思います。
# _ssm_parameter-store_write
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ssm:PutParameter",
"ssm:DeleteParameter",
"ssm:GetParameterHistory",
"ssm:GetParametersByPath",
"ssm:GetParameters",
"ssm:GetParameter",
"ssm:DeleteParameters"
],
"Resource": "arn:aws:ssm:ap-northeast-1:000000000000:parameter/*"
},
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": "ssm:DescribeParameters",
"Resource": "*"
}
]
}
S3 の IAM ポリシーは以下です。バケット名はdocker-django-blog
としています。
# _s3_docker-django-blog_write
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"s3:ListBucket",
"s3:GetBucketLocation"
],
"Resource": "arn:aws:s3:::docker-django-blog"
},
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::docker-django-blog/*"
},
{
"Sid": "VisualEditor2",
"Effect": "Allow",
"Action": [
"s3:ListAllMyBuckets",
"s3:CreateBucket"
],
"Resource": "*"
}
]
}
これらの IAM ポリシーを IAM ロールに登録し、現行サーバーにアタッチします。マネジメントコンソールで作業する想定ですが、PowerShell でやるパターンも用意してみました。
Set-DefaultAWSRegion -Region "ap-northeast-1" -Scope Script
Import-Module -Name AWS.Tools.IdentityManagement
$iamPolicies = @()
$awsAccountId = "000000000000"
$bucketName = "docker-django-blog"
# Create S3 IAM Policy
$policyName = "_s3_${bucketName}_write"
$policyDocument = @"
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"s3:ListBucket",
"s3:GetBucketLocation"
],
"Resource": "arn:aws:s3:::${bucketName}"
},
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::${bucketName}/*"
},
{
"Sid": "VisualEditor2",
"Effect": "Allow",
"Action": "s3:ListAllMyBuckets",
"Resource": "*"
}
]
}
"@
$param = @{
PolicyName = $policyName
Description = $policyName
PolicyDocument = $policyDocument
}
$s3Policy = New-IAMPolicy @param
$iamPolicies += $s3Policy
# Create SSM ParameterStore IAM Policy
$policyName = "_ssm_parameter-store_write"
$policyDocument = @"
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ssm:PutParameter",
"ssm:DeleteParameter",
"ssm:GetParameterHistory",
"ssm:GetParametersByPath",
"ssm:GetParameters",
"ssm:GetParameter",
"ssm:DeleteParameters"
],
"Resource": "arn:aws:ssm:ap-northeast-1:${awsAccountId}:parameter/*"
},
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": [
"s3:ListAllMyBuckets",
"s3:CreateBucket"
],
"Resource": "*"
}
]
}
"@
$param = @{
PolicyName = $policyName
Description = $policyName
PolicyDocument = $policyDocument
}
$ssmPolicy = New-IAMPolicy @param
$iamPolicies += $ssmPolicy
# Create IAM Role
$roleName = "_ec2_role"
$roleTag = New-Object -TypeName Amazon.IdentityManagement.Model.Tag -Property @{ Key="Name"; Value=$roleName }
$assumeRolePolicyDocument = @'
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
'@
$param = @{
AssumeRolePolicyDocument = $assumeRolePolicyDocument
RoleName = $roleName
Description = $roleName
Tag = $roleTag
}
New-IAMRole @param
# Register IAM Policy to IAM Role
$iamPolicies | ForEach-Object -Process { Register-IAMRolePolicy -PolicyArn $_.Arn -RoleName $roleName }
# Create IAM Instance Profile, Add IAM Role to IAM Instance Profile
New-IAMInstanceProfile -InstanceProfileName $roleName
Add-IAMRoleToInstanceProfile -InstanceProfileName $roleName -RoleName $roleName
Get-Variable | Remove-Variable -ErrorAction Ignore
どうしても冗長になってしまいます。CloudFormation を使ったほうがいいです。
IAM の準備ができたら、外堀を埋めていきます。Django の DB に格納されているブログコンテンツをバックアップします。Django の場合はdumpdata
を使えば簡単に取得できます。アプリに含まれるモデルをすべて取得したい場合はdumpdata <app_name>
を指定します。
python manage.py dumpdata blog --indent 4 --output blog.json
モデルごとに取得したい場合はdumpdata <app_name>.<model_name>
の形式で指定します。Site フレームワークを使っている場合、sites.site
も忘れずに取得しておきます。
python manage.py dumpdata blog.category --indent 4 --output blog.category.json
python manage.py dumpdata sites.site --indent 4 --output sites.site.json
以下のような感じで、DB データが json テキストに保存されます。
[
{
"model": "blog.tag",
"pk": "00000000000000000000000000000000",
"fields": {
"created_at": "2020-12-01T00:00:00.000Z",
"updated_at": "2020-12-02T00:00:00.000Z",
"name": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af",
"slug": "network",
"category": "11111111111111111111111111111111"
}
},
{
"model": "blog.tag",
"pk": "22222222222222222222222222222222",
"fields": {
"created_at": "2020-12-15T00:00:00.000Z",
"updated_at": "2020-12-16T00:00:00.000Z",
"name": "Ubuntu",
"slug": "ubuntu",
"category": "33333333333333333333333333333333"
}
},
# 略
]
私は以下のようなシェルスクリプトを用意して作業を簡略化しています。DB 構成に合わせてこういった簡易スクリプトを作っておくと便利です。
#!/bin/bash
models=(
sites.site
blog.category
blog.tag
blog.post
blog.snippet
blog.sitedetail
blog.aboutsite
blog.privacypolicy
blog.image
blog.link
blog
)
target_dir=$(dirname "$(cd "$(dirname "${BASH_SOURCE:-$0}")" && pwd)")
cd /django
for model in ${models[@]} ; do
python manage.py dumpdata ${model} --indent 4 --output ${target_dir}/${model}.json
done
なお、本来必要なのはblog.json
とsites.site.json
だけですが、エラー発生時に個別に対応が必要になる可能性もあるのでモデル別のファイルも出しています。
今回利用するわけではないのですが、移行時点での docker コンテナをイメージ化してバックアップしておきます。
# django コンテナをイメージ化
docker commit django.application django.application
docker save django.application > django.application.tar
# nginx コンテナをイメージ化
docker commit django.webserver django.webserver
docker save django.webserver > django.webserver.tar
# postgresql コンテナをイメージ化
docker commit django.database django.database
docker save django.database > django.database.tar
コンテナ内の定期タスクは以下のような形でホスト側で動かしています。
00 0 * * 0 docker exec -t django.webserver /usr/bin/certbot renew > /home/test-user/docker/minbase.io/nginx/log/certbot.log 2>&1
00 0 * * 1 docker exec -t django.application python /django/manage.py google_analytics_api_exec > /home/test-user/docker/minbase.io/django/log/google_analytics_api_exec.log 2>&1
30 0 * * 1 docker exec -t django.application python /django/manage.py site_report_api_exec > /home/test-user/docker/minbase.io/django/log/site_report_api_exec.log 2>&1
従って、cron
の設定もバックアップしておく必要があります。
crontab -l > crontab.backup
ここまでの手順で作成したデータは、以下のようなフォルダ構成で配置しておく想定です。
minbase.io
├ 略
└── recovery
├── cron
│ └── crontab.backup
├── data
│ ├── blog.aboutsite.json
│ ├── blog.category.json
│ ├── blog.image.json
│ ├── blog.json
│ ├── blog.link.json
│ ├── blog.post.json
│ ├── blog.privacypolicy.json
│ ├── blog.sitedetail.json
│ ├── blog.snippet.json
│ ├── blog.tag.json
│ └── sites.site.json
└── image
├── django.application.tar
├── django.database.tar
└── django.webserver.tar
ここが今回の肝かと思うのですが、EC2 起動時に流し込む UserData 内でgit
のユーザー情報が必要になるので、パラメータをセキュアな形でどこかにストアしておく必要があります。ということで、SSM パラメータストアの出番です。
aws cli で操作します。さきほど IAM ロールを付与した現行サーバーから以下のコマンドを実行します。管理しやすいように、名前を階層構造にしておきます。
aws ssm put-parameter --name "/account/git/username" --value "nekrassov01" --type String
aws ssm put-parameter --name "/account/git/password" --value "***********" --type SecureString
また今回はec2-user
とは別に、パスワード認証も公開鍵認証も可能な管理用ユーザーを UserData の中で自動生成します。これに関してもパラメータを呼び出す必要があるので、同じ要領でパラメータをセットアップしておきます。
aws ssm put-parameter --name "/account/ec2/username" --value "test-user" --type String
aws ssm put-parameter --name "/account/ec2/password" --value "***********" --type SecureString
aws ssm put-parameter --name "/account/ec2/key/public" --value "***************( 略 )***************" --type SecureString
では新しい EC2 インスタンスを立ち上げましょう。今回は手になじんでいる PowerShell で起動します。以下のようなスクリプトを書きました。CloudFormation 勉強中です。。
Set-DefaultAWSRegion -Region "ap-northeast-1" -Scope Script
Import-Module -Name AWS.Tools.EC2 -Force
$instanceName = "web-01"
# Difine Name Tag
$tag = @{ Key="Name"; Value=$instanceName }
$nameTagObj = New-Object -TypeName Amazon.EC2.Model.TagSpecification
$nameTagObj.ResourceType = "instance"
$nameTagObj.Tags.Add($tag)
# Define EBS Setting
$bd = New-Object -TypeName Amazon.EC2.Model.EbsBlockDevice
$bd.VolumeSize = 50
$bd.VolumeType = "gp3"
$bd.Iops = 3000
$bd.DeleteOnTermination = $true
$bdm = New-Object -TypeName Amazon.EC2.Model.BlockDeviceMapping
$bdm.DeviceName = "/dev/xvda"
$bdm.Ebs = $bd
# Define UserData Shell Script
$userData = @"
#!/bin/bash
timedatectl set-timezone Asia/Tokyo
localectl set-locale LANG=ja_JP.UTF8
localectl set-keymap jp106
localectl set-keymap jp-OADG109A
hostnamectl set-hostname ${instanceName}
aws configure set region ap-northeast-1
aws configure set output json
yum update -y
yum install -y docker git jq
ec2_username=`$(aws ssm get-parameters-by-path --path "/account/ec2" --with-decryption --region ap-northeast-1 | jq -r '.Parameters[1].Value')
ec2_password=`$(aws ssm get-parameters-by-path --path "/account/ec2" --with-decryption --region ap-northeast-1 | jq -r '.Parameters[0].Value')
ec2_publickey=`$(aws ssm get-parameters-by-path --path "/account/ec2/key" --with-decryption --region ap-northeast-1 | jq -r '.Parameters[0].Value')
git_username=`$(aws ssm get-parameters-by-path --path "/account/git" --with-decryption --region ap-northeast-1 | jq -r '.Parameters[1].Value')
git_password=`$(aws ssm get-parameters-by-path --path "/account/git" --with-decryption --region ap-northeast-1 | jq -r '.Parameters[0].Value')
useradd `$ec2_username
usermod -G wheel `$ec2_username
echo `$ec2_password | sudo passwd --stdin `$ec2_username
touch /etc/sudoers.d/`${ec2_username}
echo "`${ec2_username} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/`${ec2_username}
sed -i -e 's/PasswordAuthentication no/PasswordAuthentication yes/g' /etc/ssh/sshd_config
systemctl restart sshd
ssh_dir=/home/`${ec2_username}/.ssh
key_file=`${ssh_dir}/authorized_keys
mkdir -p `$ssh_dir
touch `$key_file
echo `$ec2_publickey >> `$key_file
chown `$ec2_username:`$ec2_username `$key_file
chmod 600 `$key_file
chown `${ec2_username}:`${ec2_username} `$ssh_dir
chmod 700 `$ssh_dir
systemctl start docker
systemctl enable docker
usermod -a -G docker ec2-user
usermod -a -G docker `$ec2_username
compose_dir=/opt/docker-compose
compose_version=`$(curl https://api.github.com/repos/docker/compose/releases/latest | jq .name -r)
mkdir -p `$compose_dir
git clone -b `${compose_version} "https://`${git_username}:`${git_password}@github.com/docker/compose.git" `$compose_dir
cd `$compose_dir
./script/build/linux
cp dist/docker-compose-Linux-aarch64 /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
"@ -replace "`r`n", "`n"
# Define EC2 Instance Parameters
$params = @{
MinCount = 1
MaxCount = 1
InstanceType = [Amazon.EC2.InstanceType]::T4gMicro
CreditSpecification_CpuCredit = "standard"
PrivateIpAddress = "10.0.0.10"
AssociatePublicIp = $true
KeyName = "AWS-Key"
ImageId = "ami-077527e5c50f1d6d1"
SubnetId = "subnet-00000000000000000"
SecurityGroupId = "sg-11111111111111111", "sg-2222222222222222"
BlockDeviceMapping = $bdm
EbsOptimized = $true
DisableApiTermination = $false
IamInstanceProfile_Name = "_ssm_role"
TagSpecification = $nameTagObj
EncodeUserData = $true
UserData = $userData
}
# Run EC2 Instance
$reservation = New-EC2Instance @params
$instances = $reservation.Instances
# Wait for EC2 Instance Launch
while ((Get-EC2InstanceStatus -IncludeAllInstance $true -InstanceId $instances.InstanceId).InstanceState.Name.Value -ne "running")
{
$logTime = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
Write-Host "[${logTime}] Waiting for the instances to launch..."
Start-Sleep -Seconds 15
}
Start-Sleep -Seconds 180
# Attach Elastic IP to EC2 Instance
$instances | ForEach-Object -Process {
$elasticIp = (New-EC2Address -Domain vpc).AllocationId
Register-EC2Address -InstanceId $_.InstanceId -AllocationId $elasticIp
}
# Cleanup Variables
Get-Variable | Remove-Variable -ErrorAction Ignore
見づらいので、UserData のみ切り出して以下に貼っておきます。
#!/bin/bash
timedatectl set-timezone Asia/Tokyo
localectl set-locale LANG=ja_JP.UTF8
localectl set-keymap jp106
localectl set-keymap jp-OADG109A
hostnamectl set-hostname web-01
aws configure set region ap-northeast-1
aws configure set output json
yum update -y
yum install -y docker git jq
ec2_username=$(aws ssm get-parameters-by-path --path "/account/ec2" --with-decryption --region ap-northeast-1 | jq -r '.Parameters[1].Value')
ec2_password=$(aws ssm get-parameters-by-path --path "/account/ec2" --with-decryption --region ap-northeast-1 | jq -r '.Parameters[0].Value')
ec2_publickey=$(aws ssm get-parameters-by-path --path "/account/ec2/key" --with-decryption --region ap-northeast-1 | jq -r '.Parameters[0].Value')
git_username=$(aws ssm get-parameters-by-path --path "/account/git" --with-decryption --region ap-northeast-1 | jq -r '.Parameters[1].Value')
git_password=$(aws ssm get-parameters-by-path --path "/account/git" --with-decryption --region ap-northeast-1 | jq -r '.Parameters[0].Value')
useradd $ec2_username
usermod -G wheel $ec2_username
echo $ec2_password | sudo passwd --stdin $ec2_username
touch /etc/sudoers.d/${ec2_username}
echo "${ec2_username} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/${ec2_username}
sed -i -e 's/PasswordAuthentication no/PasswordAuthentication yes/g' /etc/ssh/sshd_config
systemctl restart sshd
ssh_dir=/home/${ec2_username}/.ssh
key_file=${ssh_dir}/authorized_keys
mkdir -p $ssh_dir
touch $key_file
echo $ec2_publickey >> $key_file
chown ${ec2_username}:${ec2_username} $key_file
chmod 600 $key_file
chown ${ec2_username}:${ec2_username} $ssh_dir
chmod 700 $ssh_dir
systemctl start docker
systemctl enable docker
usermod -a -G docker ec2-user
usermod -a -G docker $ec2_username
compose_dir=/opt/docker-compose
compose_version=$(curl https://api.github.com/repos/docker/compose/releases/latest | jq .name -r)
mkdir -p $compose_dir
git clone -b ${compose_version} "https://${git_username}:${git_password}@github.com/docker/compose.git" $compose_dir
cd $compose_dir
./script/build/linux
cp dist/docker-compose-Linux-aarch64 /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
かいつまんで説明します。まず EC2 につける Name タグはオブジェクトで定義する必要があります。
# Define Name Tag
$tag = @{ Key="Name"; Value=$instanceName }
$nameTagObj = New-Object -TypeName Amazon.EC2.Model.TagSpecification
$nameTagObj.ResourceType = "instance"
$nameTagObj.Tags.Add($tag)
EBS のオブジェクトを作ります。gp3
を指定し、IOPS はベースラインパフォーマンスである3000を指定します。ちなみに今回は明示的に指定していますが、初期値が3000なので書かなくても問題ありません。
# Define EBS Setting
$bd = New-Object -TypeName Amazon.EC2.Model.EbsBlockDevice
$bd.VolumeSize = 50
$bd.VolumeType = "gp3"
$bd.Iops = 3000
$bd.DeleteOnTermination = $true
$bdm = New-Object -TypeName Amazon.EC2.Model.BlockDeviceMapping
$bdm.DeviceName = "/dev/xvda"
$bdm.Ebs = $bd
そして UserData です。冒頭ではタイムゾーンやロケールの設定をしています。
timedatectl set-timezone Asia/Tokyo
localectl set-locale LANG=ja_JP.UTF8
localectl set-keymap jp106
localectl set-keymap jp-OADG109A
ホスト名は、ひとつ上のレイヤーである PowerShell 側で Name タグとして定義した文字列を埋め込んでいます。
hostnamectl set-hostname ${instanceName}
aws configure
のセットアップではリージョン、出力フォーマットを指定しています。あとは IAM ロールがよしなにやってくれます。
aws configure set region ap-northeast-1
aws configure set output json
yum
を最新化して必要なツールをインストールします。jq
は、UserData が動作する中で各パラメータを動的に取得するために必要です。
yum update -y
yum install -y docker git jq
今回の肝となる部分ですが、SSM パラメータストアから機密情報を取得します。新規に作成する管理用ユーザーの名前、パスワード、公開鍵情報、およびgit
のログイン情報を取得しています。
ec2_username=`$(aws ssm get-parameters-by-path --path "/account/ec2" --with-decryption --region ap-northeast-1 | jq -r '.Parameters[1].Value')
ec2_password=`$(aws ssm get-parameters-by-path --path "/account/ec2" --with-decryption --region ap-northeast-1 | jq -r '.Parameters[0].Value')
ec2_publickey=`$(aws ssm get-parameters-by-path --path "/account/ec2/key" --with-decryption --region ap-northeast-1 | jq -r '.Parameters[0].Value')
git_username=`$(aws ssm get-parameters-by-path --path "/account/git" --with-decryption --region ap-northeast-1 | jq -r '.Parameters[1].Value')
git_password=`$(aws ssm get-parameters-by-path --path "/account/git" --with-decryption --region ap-northeast-1 | jq -r '.Parameters[0].Value')
ec2-user
のほかに管理用ユーザーを作成し、パスワード認証が可能な状態にします。
useradd `$ec2_username
usermod -G wheel `$ec2_username
echo `$ec2_password | sudo passwd --stdin `$ec2_username
touch /etc/sudoers.d/`${ec2_username}
echo "`${ec2_username} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/`${ec2_username}
sed -i -e 's/PasswordAuthentication no/PasswordAuthentication yes/g' /etc/ssh/sshd_config
systemctl restart sshd
この管理用ユーザーについては、公開鍵認証もセットアップします。
ssh_dir=/home/`${ec2_username}/.ssh
key_file=`${ssh_dir}/authorized_keys
mkdir -p `$ssh_dir
touch `$key_file
echo `$ec2_publickey >> `$key_file
chown `${ec2_username}:`${ec2_username} `$key_file
chmod 600 `$key_file
chown `${ec2_username}:`${ec2_username} `$ssh_dir
chmod 700 `$ssh_dir
docker
を自動起動するようにし、sudo
なしで使えるようにします。
systemctl start docker
systemctl enable docker
usermod -a -G docker ec2-user
usermod -a -G docker `$ec2_username
docker-compose
は arm64 アーキテクチャ用のバイナリが用意されていないので、ソースからビルドする必要があります。UserData の中でどう実現するかというところで、以下の形に落ち着きました。パラメータストアからgit
のログイン情報を取得してgit clone
し、ビルドスクリプトを走らせます。その際、API で取得した latest バージョン番号を指定するようにします。
# docker-compose のソース保存場所
compose_dir=/opt/docker-compose
# API で docker-compose の latest バージョン番号を取得する
compose_version=`$(curl https://api.github.com/repos/docker/compose/releases/latest | jq .name -r)
# arm64 版の docker-compose バイナリは用意されていないので、ソースからビルドする必要がある
# バージョンを指定して compose をクローンする
# 認証でコケないために、パラメータストアで取得した git のログイン情報を埋め込んでアクセスする
mkdir -p `$compose_dir
git clone -b `${compose_version} "https://`${git_username}:`${git_password}@github.com/docker/compose.git" /opt/docker-compose
# 用意されているビルドスクリプトを実行する
cd /opt/docker-compose/
./script/build/linux
# ビルドしたバイナリにアクセスできるようにリンクや権限を整備する
cp dist/docker-compose-Linux-aarch64 /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
ちなみに、ビルドが完了するまではかなり時間がかかるので、アクセス可能となってからもまだdocker-compose
できる状態になっていません。気になる場合は以下のコマンドで眺めましょう。
tail -f /var/log/cloud-init-output.log
大量のパラメータでNew-EC2Instance
します。インスタンスタイプはもちろんt4g
にします。冒頭で作成した IAM ロールも忘れずにアタッチします。
# インスタンス起動
$params = @{
MinCount = 1
MaxCount = 1
InstanceType = [Amazon.EC2.InstanceType]::T4gMicro
CreditSpecification_CpuCredit = "standard"
PrivateIpAddress = "10.0.0.10"
AssociatePublicIp = $true
KeyName = "AWS-Key"
ImageId = "ami-077527e5c50f1d6d1"
SubnetId = "subnet-00000000000000000"
SecurityGroupId = "sg-11111111111111111", "sg-22222222222222222"
BlockDeviceMapping = $bdm
EbsOptimized = $true
DisableApiTermination = $true
IamInstanceProfile_Name = "_ec2_role"
TagSpecification = $nameTagObj
EncodeUserData = $true
UserData = $userData
}
$reservation = New-EC2Instance @params
$instances = $reservation.Instances
起動するまで待機します。初期化中にアクセスしないよう、最後に180秒のウェイトを入れています。
while ((Get-EC2InstanceStatus -IncludeAllInstance $true -InstanceId $instances.InstanceId).InstanceState.Name.Value -ne "running")
{
$logTime = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
Write-Host "[${logTime}] Waiting for the instances to launch..."
Start-Sleep -Seconds 15
}
Start-Sleep -Seconds 180
インスタンスが完全に起動したら ElasticIP を付与します。
$instances | ForEach-Object -Process {
$elasticIp = (New-EC2Address -Domain vpc).AllocationId
Register-EC2Address -InstanceId $_.InstanceId -AllocationId $elasticIp
}
手っ取り早く済ませるために、ディレクトリ構造ごとそのまま S3 にアップロードしてしまいます。以下のコマンドでバケットを用意しましょう。
aws s3 mb s3://docker-django-blog
続いて現行サーバーにある資材をすべて S3 に同期します。サブコマンドsync
がとても便利です。アプリのルートディレクトリ(冒頭のフォルダ構成に従えば minbase.io フォルダ)に移動してコマンドを実行します。
aws s3 sync . s3://docker-django-blog/minbase.io
ここからは新サーバー側で作業します。S3 から資材を持ってくる必要があります。任意の場所で以下のコマンドを実行します。
mkdir minbase.io && cd minbase.io
aws s3 sync s3://docker-django-blog/minbase.io .
sync
の source/destination を反転させただけです。ここまでの作業で、docker-compose
によるデプロイが可能な状態になったはずです。カレントディレクトリにdocker-compose.yml
があることを確認し、おもむろに以下のコマンドを叩きましょう。
docker-compose up -d --build
コーヒーでも飲んで完了を待ちます。docker-compose
バイナリをビルドするときもそうですが、最小構成でやっている場合、スペックが足りずに落ちたりすることもあります。一時的にインスタンスタイプを調整しながらの作業になると思います。
この時点ではまだ、ブラウザに Web サイトは表示されません。いくつか設定をいじる必要があります。証明書がなく HTTPS ではアクセスできなくなっているので、いったん HTTP の設定に戻します。
アプリケーション | ファイル | 確認事項 |
---|---|---|
Django | settings.py | ALLOWED_HOSTS に自分自身の IP を入れるかワイルドカードを指定する |
Django | settings.py | SECURE_SSL_REDIRECT がTrue の場合はFalse にする |
Nginx | default.conf | HTTP アクセス用の設定に差し替える |
上記が終わったら、Django の初期設定を行います。まずdjango.application
コンテナに入ります。
docker exec -it django.application /bin/bash
DB を migrate して、superuser を作って、static ファイルを配信します。
python manage.py migrate
python manage.py createsuperuser
python manage.py collectstatic
ここまで来たら、EC2 のセキュリティグループで80番が許可されていることを確認し、ブラウザで IP を直打ちしてみましょう。以下のように、まっさらな Web サイトが表示されれば成功です。
そして、このまっさらなブログに対して DB データをロードします。コンテナ内で作業するので、事前にdumpdata
を永続化されたボリューム内に移動しておきます。
cp recovery/data/blog.json django/blog/fixtures/
cp recovery/data/sites.site.json django/blog/fixtures/
再度コンテナに入り、
docker exec -it django.application /bin/bash
データをロードします。
python manage.py loaddata blog/fixtures/sites.site.json
python manage.py loaddata blog/fixtures/blog.json
エラーが出なければ、これでブログコンテンツはすべて新環境にリプレースされたことになります。メインの作業は完了です。
Django の定期タスクが組まれている crontab を復元します。コマンド一発です。
crontab recovery/cron/crontab.backup
コンテンツが移行できたので、ElasticIP を付け替えて、ドメイン名でアクセスしたときに新サーバーにルーティングされるようにします。この作業はマネジメントコンソールから手動で行います。手順は割愛。
最後の工程になります。新サーバーではまだ証明書の設定を行っていないため、設定していきます。まずは旧サーバー側で証明書をキッチリ失効させておきましょう。ちなみに証明書は Let's Encrypt で運用しています。ここではdjango.webserver
コンテナに入って作業します。
# 旧サーバー側作業
docker exec -it django.weberver /bin/bash
コンテナの中に入ったら、まず証明書の内容を確認します。
sudo certbot certificates
確認できたら、証明書を失効します。
sudo certbot revoke --cert-path /etc/letsencrypt/live/minbase.io/cert.pem
失効すると、certbot
のバージョンによっては、ついでに関連ファイルを削除するか聞かれますのでy
を返しましょう。明示的な削除コマンドは以下です。
sudo certbot delete --cert-name minbase.io
続いて新サーバーで証明書を発行します。django.webserver
コンテナに入り、
# 新サーバー側作業
docker exec -it django.weberver /bin/bash
証明書を発行します。
certbot certonly \
--webroot \
-w /static \
-d minbase.io \
-d www.minbase.io \
-m <email_address> \
--agree-tos
以下は Let's Encrypt 公式の引用です。
Let's Encrypt から証明書を取得するときには、ACME 標準で定義されている「チャレンジ」を使用して、証明書が証明しようとしているドメイン名があなたの制御下にあることを検証します。(中略)Let’s Encrypt は ACME クライアントにトークンを発行し、ACME クライアントはウェブサーバー上の http://<domain>/.well-known/acme-challenge/という場所に1つのファイルを設置します。このファイルにはトークンに加えて、あなたのアカウントのキーのフィンガープリントが含まれています。ACME クライアントが Let’s Encrypt にファイルが準備できたことを伝えると、Let’s Encrypt はそのファイルを取得しようとします ( 複数の有効な場所から複数回取得する可能性もあります )。私たちの検証チェックであなたのウェブサーバーから正当なレスポンスが得られた場合、検証は成功したとみなされ、証明書の発行に進むことができます。検証チェックに失敗した場合には、新しい証明書の発行を再試行する必要があります。
証明書を正常に発行するには、certbot
の証明書発行プロセスがhttp://<domain>/.well-known/acme-challenge/
にアクセスできるようにしてあげる必要があります。これに関しては、Django の場合は静的ファイル配置場所を指定することでパスできます。今回のケースはdjango.webserver
コンテナ内の/static
を指定しています。あとは、nginx の conf に以下の記述があれば OK です。
server {
・・・
location ^~ /.well-known/acme-challenge/ {
root /static;
}
・・・
}
これで、証明書の発行が通るはずです。あとは、一時的に HTTP アクセス用の設定にしてあったところを、本来の HTTPS アクセス用の設定に戻し、リクエストがhttps://
にリダイレクトされるかを確認して正常であれば、すべての作業が完了となります。また後始末としては、S3 に配置した資材は削除しておくのが無難でしょう。
EC2, Docker, Django の組み合わせでのサーバー移行は初めてだったので、試行錯誤しながら進めていった感じでしたが、SSM パラメータストアを利用した EC2 初期設定の自動化等、副産物が大きかったと思います。久々にブログを書きましたが他にも書きたいことが溜まってきているので、時間を見つけて執筆したいと思います。
追記 :
最小構成で運用する場合、ルートボリュームに swap 領域を確保したほうがよかったのですが、すっかり忘れていました。こちらの記事を参考に実装しています。