この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
みなさん、こんにちは!
AWS事業本部の青柳@福岡オフィスです。
re:Invent 2019 で発表された、第2世代のARMベースプロセッサ「Graviton2」を搭載した「M6gインスタンス」が、先日GAとなりました。
この新しいインスタンスタイプを使って何かやってみよう! ということで、Graviton2インスタンスで Docker を使っていろいろと試してみます。
その1: M6gインスタンスを起動してDockerを動かしてみる
インスタンス起動
AMIの選択で「Amazon Linux 2」を選択して、アーキテクチャは「64ビット (Arm)」を選択します。
インスタンスタイプの選択で、第1世代のARMベースインスタンス「A1」に加えて、第2世代のARMベースインスタンス「M6g」が選択できるようになっています。
あとは、適宜設定していってインスタンスを起動してください。
ポイントは以下の通りです。
- Dockerイメージを保存するために、ディスク容量は若干増やしておいた方が良いかもです。(例:8GB→20GB)
- 動作テストを行うために、セキュリティグループのインバウンドルールで「マイIP」からの「TCP/80」および「TCP/8080」の接続を許可してください。
CloudFormationで環境構築する場合は、以下のテンプレートを利用してください。
CloudFormationテンプレート (クリックすると展開します)
cfn-graviton2.yaml
---
AWSTemplateFormatVersion: "2010-09-09"
Description: "Launch 'Graviton2' EC2 instance with VPC environment"
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: "General Information"
Parameters:
- SystemName
- Label:
default: "Network Configuration"
Parameters:
- CidrBlockVPC
- CidrBlockSubnetPublic
- MyIpAddressCidr
- Label:
default: "EC2 Instance Configuration"
Parameters:
- Graviton2ImageID
- Graviton2InstanceType
- Graviton2KeyName
- Graviton2VolumeType
- Graviton2VolumeSize
Parameters:
SystemName:
Type: String
Default: graviton2
CidrBlockVPC:
Type: String
Default: 192.168.0.0/16
CidrBlockSubnetPublic:
Type: String
Default: 192.168.1.0/24
MyIpAddressCidr:
Type: String
Graviton2ImageID:
Type: AWS::SSM::Parameter::Value<String>
Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-arm64-gp2
Graviton2InstanceType:
Type: String
Default: m6g.medium
AllowedValues:
- m6g.medium
- m6g.large
- m6g.xlarge
- m6g.2xlarge
- m6g.4xlarge
- m6g.8xlarge
- m6g.12xlarge
- m6g.16xlarge
Graviton2KeyName:
Type: AWS::EC2::KeyPair::KeyName
Graviton2VolumeType:
Type: String
Default: gp2
Graviton2VolumeSize:
Type: String
Default: 20
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref CidrBlockVPC
EnableDnsSupport: true
EnableDnsHostnames: true
InstanceTenancy: default
Tags:
- Key: Name
Value: !Sub "${SystemName}-vpc"
- Key: System
Value: !Ref SystemName
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub "${SystemName}-igw"
- Key: System
Value: !Ref SystemName
VPCGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId: !Ref InternetGateway
VpcId: !Ref VPC
SubnetPublic:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select
- 0
- Fn::GetAZs: !Ref AWS::Region
CidrBlock: !Ref CidrBlockSubnetPublic
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub "${SystemName}-public-subnet"
- Key: System
Value: !Ref SystemName
RouteTablePublic:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub "${SystemName}-public-rtb"
- Key: System
Value: !Ref SystemName
RouteIGW:
DependsOn:
- VPCGatewayAttachment
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref RouteTablePublic
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
RouteTableAssociationPublic:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref SubnetPublic
RouteTableId: !Ref RouteTablePublic
SecurityGroupServer:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: !Sub "${SystemName}-server-sg"
GroupDescription: "Security group for server"
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: !Ref MyIpAddressCidr
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: !Ref MyIpAddressCidr
- IpProtocol: tcp
FromPort: 8080
ToPort: 8080
CidrIp: !Ref MyIpAddressCidr
Tags:
- Key: Name
Value: !Sub "${SystemName}-server-sg"
- Key: System
Value: !Ref SystemName
EC2InstanceGraviton2:
Type: AWS::EC2::Instance
Properties:
ImageId: !Ref Graviton2ImageID
InstanceType: !Ref Graviton2InstanceType
KeyName: !Ref Graviton2KeyName
BlockDeviceMappings:
- DeviceName: /dev/xvda
Ebs:
VolumeType: !Ref Graviton2VolumeType
VolumeSize: !Ref Graviton2VolumeSize
NetworkInterfaces:
- DeviceIndex: 0
SubnetId: !Ref SubnetPublic
GroupSet:
- !Ref SecurityGroupServer
Tags:
- Key: Name
Value: !Sub "${SystemName}-server"
- Key: System
Value: !Ref SystemName
Outputs:
VPC:
Value: !Ref VPC
Export:
Name: !Sub "${AWS::StackName}::VPC"
SubnetPublic:
Value: !Ref SubnetPublic
Export:
Name: !Sub "${AWS::StackName}::SubnetPublic"
SecurityGroupServer:
Value: !Ref SecurityGroupServer
Export:
Name: !Sub "${AWS::StackName}::SecurityGroupServer"
EC2InstanceGraviton2:
Value: !Ref EC2InstanceGraviton2
Export:
Name: !Sub "${AWS::StackName}::EC2InstanceGraviton2"
Dockerのインストール
インスタンスが起動しましたら、SSHで接続します。
「Amazon Linux Extras」を使ってDockerをインストールします。
$ sudo amazon-linux-extras install docker=latest
サービス自動起動の設定を行います。
$ sudo systemctl enable docker.service
$ sudo systemctl start docker.service
「ec2-user」ユーザーを「docker」グループに所属させます。
(docekr
コマンドをsudo
を付けずに実行できるようにするための設定です)
$ sudo usermod -aG docker ec2-user
一度ログアウトしてから、再度ログインします。
docker
コマンドが実行できることを確認しましょう。
$ docker version
Client:
Version: 19.03.6-ce
API version: 1.40
Go version: go1.13.4
Git commit: 369ce74
Built: Fri Apr 24 18:30:35 2020
OS/Arch: linux/arm64
Experimental: false
Server:
Engine:
Version: 19.03.6-ce
API version: 1.40 (minimum version 1.12)
Go version: go1.13.4
Git commit: 369ce74
Built: Fri Apr 24 18:31:36 2020
OS/Arch: linux/arm64
Experimental: false
containerd:
Version: 1.3.2
GitCommit: ff48f57fc83a8c44cf4ad5d672424a98ba37ded6
runc:
Version: 1.0.0-rc10
GitCommit: dc9208a3303feef5b3839f4323d9beb36df0a9dd
docker-init:
Version: 0.18.0
GitCommit: fec3683
Dockerコンテナの実行
試しに「nginx」の公式コンテナイメージを使ってコンテナを起動してみます。
$ docker container run --name nginx -p 80:80 --rm nginx:latest
Unable to find image 'nginx:latest' locally
latest: Pulling from library/nginx
b24dc5b5f4f0: Pull complete
8a4ea8d1205e: Pull complete
772e9fa36a03: Pull complete
Digest: sha256:404ed8de56dd47adadadf9e2641b1ba6ad5ce69abf251421f91d7601a2808ebe
Status: Downloaded newer image for nginx:latest
TCP/80ポートで待ち受け状態になります。
WebブラウザでインスタンスのIPアドレスにアクセスすると、Nginxのページが表示されました。
その2: M6gインスタンス上でDockerイメージをビルドしてみる
Go言語環境のインストール
今回は、Go言語 (Golang) で記述したプログラムをDockerコンテナで動かしてみます。
YumでGo言語環境をインストールします。
$ sudo yum install golang
インストールされたことを確認します。
$ go version
go version go1.13.4 linux/arm64
Go言語プログラムのビルド
ソースコードを格納するディレクトリを作成します。
$ mkdir go-webserver-sample
$ cd go-webserver-sample
以下の内容でソースコードを保存します。
go-webserver-sample.go
package main
import (
"fmt"
"net/http"
"os"
"runtime"
)
func handler(w http.ResponseWriter, r *http.Request) {
hostname, _ := os.Hostname()
fmt.Fprintf(w, "<h1>Welcome Golang-WebServer!</h1>")
fmt.Fprintf(w, "<h2>Hostname: %s</h2>", hostname)
fmt.Fprintf(w, "<h2>OS: %s</h2>", runtime.GOOS)
fmt.Fprintf(w, "<h2>Architecture: %s</h2>", runtime.GOARCH)
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
ソースコードをビルド (コンパイル) します。
$ go build go-webserver-sample.go
実行可能ファイルとして、拡張子無しのファイルgo-webserver-sample
が作成されます。
$ ls -l
total 6832
-rwxrwxr-x 1 ec2-user ec2-user 7049401 May 14 08:14 go-webserver-sample
-rw-rw-r-- 1 ec2-user ec2-user 480 May 14 08:00 go-webserver-sample.go
動作確認のために、実行可能ファイルを実行してみましょう。
$ ./go-webserver-sample
TCP/8080ポートで待ち受け状態になります。
Webブラウザで「http://インスタンスのIPアドレス:8080」にアクセスすると、以下のような画面が表示されると思います。
Golangプログラムを含むDockerイメージを作成
さきほどビルドした実行可能ファイルですが、Dockerコンテナで実行させるためにはビルドオプションの追加が必要です。
Go言語は実行可能ファイルをシングルバイナリで生成してくれる言語ですが、通常のビルドオプションではライブラリへのリンク方式が「動的リンク」(実行時リンク) になります。
ビルドを行う環境 (=EC2インスタンス) と実行する環境 (=Dockerコンテナ) が異なる場合、実行時にライブラリへの動的リンクが上手く行えないことがあります。
これを避けるためには、ライブラリへのリンク方式を「静的リンク」(ビルド時リンク) にする必要があります。
静的リンクによるビルドを行うには、以下のようにコマンドを実行します。
$ CGO_ENABLED=0 go build -a -installsuffix cgo go-webserver-sample.go
- シェル変数
CGO_ENABLED
の値を0
に設定 go build
コマンドのオプション-a
および-installsuffix cgo
を指定
実行可能ファイルを再生成しましたので、Dockerイメージの作成を行いましょう。
以下の内容でDockerfileを保存します。
Dockerfile
FROM alpine:latest
COPY ./go-webserver-sample /bin/
CMD ["/bin/go-webserver-sample"]
内容としては単純で、軽量Linuxである「Alpine」をベースイメージとして、実行可能ファイルをホストからコンテナへコピーして起動しているだけです。
Docekrイメージのビルドを行います。
$ docker image build -t go-webserver-sample:v1 .
Sending build context to Docker daemon 7.053MB
Step 1/3 : FROM alpine:latest
latest: Pulling from library/alpine
29e5d40040c1: Pull complete
Digest: sha256:9a839e63dad54c3a6d1834e29692c8492d93f90c59c978c1ed79109ea4fb9a54
Status: Downloaded newer image for alpine:latest
---> c20d2a9ab686
Step 2/3 : COPY ./go-webserver-sample /bin/
---> 6ead0d290f19
Step 3/3 : CMD ["/bin/go-webserver-sample"]
---> Running in af0556cc7bd7
Removing intermediate container af0556cc7bd7
---> 7f57238e2752
Successfully built 7f57238e2752
Successfully tagged go-webserver-sample:v1
ビルドしたDockerイメージを確認しましょう。
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
go-webserver-sample v1 7f57238e2752 5 seconds ago 12.4MB
alpine latest c20d2a9ab686 3 weeks ago 5.36MB
コンテナを起動してみます。
$ docker container run -p 80:8080 --rm go-webserver-sample:v1
「TCP/8080」→「TCP/80」へのポートマッピングにより、TCP/80ポートで待ち受け状態になります。
WebブラウザでインスタンスのIPアドレスにアクセスすると、以下のようなページが表示されると思います。
(「Hostname」の表示が、さきほどはEC2インスタンスのホスト名でしたが、今回はコンテナのホスト名となっています)
その3: コンテナ上でGo言語のビルドから実行まで行ってみる
さきほどはホスト上でGo言語のビルドを行いましたが、Dokcerコンテナ上でGo言語のビルドを行うこともできます。
そうすることで、ホスト側にGo言語環境が無くてもソースコードを用意するだけでDockerイメージをビルドすることができます。
Dockerfileの記述
Dockerfileを以下の内容に書き換えます。
Dockerfile
FROM golang:latest
WORKDIR /tmp
COPY ./go-webserver-sample.go /tmp
RUN CGO_ENABLED=0 go build -a -installsuffix cgo go-webserver-sample.go
RUN mv /tmp/go-webserver-sample /bin/
CMD ["/bin/go-webserver-sample"]
前節ではベースイメージとして「Alpine」を用いましたが、AlpineにはGo言語をビルドするための環境が含まれていません。
そこで、Go言語環境がインストール済みのコンテナである「golang」をベースイメージとして用いることにします。(1行目)
まず、Go言語のビルドを行います。
- 2行目: 作業用ディレクトリとして「/tmp」を指定します
- 3行目: ホストからコンテナへソースコードファイルをコピーします
- 4行目: Go言語のビルドを実行します
ビルドを行った後は、生成された実行可能ファイルを起動します。
- 5行目: ビルド済み実行可能ファイルを「/tmp」から「/bin」へ移動します
- 6行目: 実行可能ファイルを起動します
Dockerイメージのビルド
それでは、Docekrイメージのビルドを行います。
$ docker image build -t go-webserver-sample:v2 .
Sending build context to Docker daemon 3.072kB
Step 1/6 : FROM golang:latest
latest: Pulling from library/golang
d23bf71de5e1: Pull complete
d4f6b089b352: Pull complete
f34690136adb: Pull complete
4287f76f52e4: Pull complete
d5631a7a4659: Pull complete
2a7b5302103f: Pull complete
87401a001075: Pull complete
Digest: sha256:b5114a530de5817bcc9b9b5f7b523b0424b75c78dd2f68d2b6d79dc858d98c9f
Status: Downloaded newer image for golang:latest
---> 0d65fe43068f
Step 2/6 : WORKDIR /tmp
---> Running in b48ac73a2e10
Removing intermediate container b48ac73a2e10
---> 92d25a8aeaf2
Step 3/6 : COPY ./go-webserver-sample.go /tmp
---> 9165f87ea84c
Step 4/6 : RUN CGO_ENABLED=0 go build -a -installsuffix cgo go-webserver-sample.go
---> Running in ad1b31d928b8
Removing intermediate container ad1b31d928b8
---> 35587c993f72
Step 5/6 : RUN mv /tmp/go-webserver-sample /bin/
---> Running in c8b096b1af11
Removing intermediate container c8b096b1af11
---> af3526e1ccc3
Step 6/6 : CMD ["/bin/go-webserver-sample"]
---> Running in 94d2bb17446a
Removing intermediate container 94d2bb17446a
---> 2b8410d3e9e4
Successfully built 2b8410d3e9e4
Successfully tagged go-webserver-sample:v2
ビルドしたDockerイメージを確認しましょう。
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
go-webserver-sample v2 2b8410d3e9e4 About a minute ago 755MB
go-webserver-sample v1 7f57238e2752 3 minutes ago 12.4MB
golang latest 0d65fe43068f 22 hours ago 714MB
alpine latest c20d2a9ab686 3 weeks ago 5.36MB
コンテナを起動してみます。
$ docker container run -p 80:8080 --rm go-webserver-sample:v2
WebブラウザでインスタンスのIPアドレスにアクセスすると、以下のようなページが表示されると思います。
(コンテナのホスト名が異なるのみで、出力結果は前節と変わりませんね)
生成されたDockerイメージの「大きさ」を比べてみると・・・
Dockerコンテナ上でGo言語のビルドを行うことでホスト側にGo言語環境が不要となった訳ですが、ここで、ビルド後のDockerイメージを前節と本節とで比べてみましょう。
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
go-webserver-sample v2 2b8410d3e9e4 About a minute ago 755MB
go-webserver-sample v1 7f57238e2752 3 minutes ago 12.4MB
golang latest 0d65fe43068f 22 hours ago 714MB
alpine latest c20d2a9ab686 3 weeks ago 5.36MB
前節では軽量Linuxイメージ「Alpine」をベースにしたおかげでサイズが「約12MB」と小さくなっていますが、本節ではGo言語環境一式を含む「golang」をベースにしたためサイズが「755MB」とかなり大きくなってしまっています。
イメージのサイズを小さく抑える方法として、次節では「マルチステージビルド」をご紹介します。
その4: マルチステージビルドを使ってみる
「マルチステージビルド」とは?
「マルチステージビルド」とは、一つのDockerfileの中で複数のステップ (ステージ) を記述することで、最終的に生成されるDockerイメージのサイズを抑えることができる仕組みです。
Use multi-stage builds | Docker Documentation
具体的なDockerfileの記述内容を見て行きましょう。
Dockerfile
FROM golang:latest AS builder
WORKDIR /tmp
COPY ./go-webserver-sample.go /tmp
RUN CGO_ENABLED=0 go build -a -installsuffix cgo go-webserver-sample.go
FROM alpine:latest
COPY --from=builder /tmp/go-webserver-sample /bin/
CMD ["/bin/go-webserver-sample"]
イメージを定義するブロックが2つ書かれています。
1つ目のブロックは「ビルド用コンテナ」すなわち、Go言語のソースコードをビルドして実行可能ファイルを生成するためのコンテナイメージを定義しています。
- 1行目: ベースイメージとして、Go言語環境インストール済みのLinuxイメージである「golang」を指定します (
AS builder
は後からビルド用コンテナの参照を可能にするためのラベルです) - 2行目: 作業用ディレクトリとして「/tmp」を指定します
- 3行目: ホストからコンテナへソースコードファイルをコピーします
- 4行目: Go言語のビルドを実行します
2つ目のブロックは「実行用コンテナ」であり、最終的に出力されるコンテナイメージとなります。
- 6行目: ベースイメージとして「alpine」を指定します。
- 7行目: 「builder」のラベルが付いたコンテナ (=1つ目のブロックで定義されたビルド用コンテナ) からビルド済み実行可能ファイルをコピーします
- 8行目: 実行可能ファイルを起動します
マルチステージビルドによるDockerイメージ作成
では、書き換えたDockerfileを用いてマルチステージビルドを行いましょう。
$ docker image build -t go-webserver-sample:v3 .
Sending build context to Docker daemon 3.072kB
Step 1/7 : FROM golang:latest AS builder
---> 0d65fe43068f
Step 2/7 : WORKDIR /tmp
---> Using cache
---> 92d25a8aeaf2
Step 3/7 : COPY ./go-webserver-sample.go /tmp
---> Using cache
---> 9165f87ea84c
Step 4/7 : RUN CGO_ENABLED=0 go build -a -installsuffix cgo go-webserver-sample.go
---> Using cache
---> 35587c993f72
Step 5/7 : FROM alpine:latest
---> c20d2a9ab686
Step 6/7 : COPY --from=builder /tmp/go-webserver-sample /bin/
---> 36e81eb0d39b
Step 7/7 : CMD ["/bin/go-webserver-sample"]
---> Running in ca917a44a9ca
Removing intermediate container ca917a44a9ca
---> fe0599c9e44d
Successfully built fe0599c9e44d
Successfully tagged go-webserver-sample:v3
ビルドしたイメージを確認します。
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
go-webserver-sample v3 fe0599c9e44d 31 seconds ago 12.5MB
go-webserver-sample v2 2b8410d3e9e4 3 minutes ago 755MB
go-webserver-sample v1 7f57238e2752 5 minutes ago 12.4MB
golang latest 0d65fe43068f 22 hours ago 714MB
alpine latest c20d2a9ab686 3 weeks ago 5.36MB
最終的に生成されたイメージ「go-webserver-sample:v3」は、前々節で作成した「alpine」ベースのイメージ「go-webserver-sample:v1」とほぼ同じサイズであることが分かります。
最後に、コンテナを起動します。
$ docker container run -p 80:8080 --rm go-webserver-sample:v3
前節、前々節と同様にWebブラウザでアクセスしてみます。
前節と同様に出力結果は変わりません。
おわりに
「Graviton2」上でDockerを使ってコンテナイメージをビルドしたり実行したりするいくつかの方法を試してみました。
次回は「x86」アーキテクチャのDocker環境で「Graviton2」アーキテクチャ用のコンテナイメージをビルドしてみたいと思います。