前書き
初心者向けに自分なりの言葉で柔らかく書いたつもりです。非エンジニアですが、メモ目的とアウトプットの恩恵のために記事にしました。間違っているところなどありましたら、コメントをください。
あとコードにコメントが多く可読性が悪いかもしれません。そこは申し訳なく…
iosからAWSまでの一連の流れを記事にしているためかなり長くなっているので、記事を分けて書いています。
- RailsとiOS(Swift)とAWS間をAPI通信で、データファイル保存させるまで①devise token auth編
- RailsとiOS(Swift)とAWS間をAPI通信で、データファイル保存させるまで②Alamofire、Swiftyjson編 devise_token_auth(認証)のデータの送受信
- RailsとiOS(Swift)とAWS間をAPI通信で、データファイル保存させるまで③Alamofire、Swiftyjson編 apiモードを使ってdevise_token_auth(認証)以外のデータの送受信
③までのものがこちら
https://github.com/ToruMizuno/railswiftapi
④までのものがこちら
https://github.com/ToruMizuno/railswiftapi2
carrierwave編
参考記事
- 【Rails】S3へ『CarrierWave+fog』を使って画像アップロードする方法
- Railsを使って、S3にファイルをアップロードする。
- Railsのjbuilderの書き方と便利なイディオムやメソッド
- rails ユーザー画像アップロードの手順
- [Rails5] Carrierwave + S3 + CloudFrontで画像アップロードを実装する方法
- RailsにCloudFront噛ませたら10倍速くなった。RailsのエッジサーバとしてのCloudFrontの有用性について
- carrierwaveでS3にアップした画像に有効期限をつけると共にCloud Frontで配信するための手順
- Rails(4.2) + CarrierWaveにiOSからAlamofireを用いて画像をPOSTする
事前準備
(メールアドレス作成と)AWSアカウント作成
AWS アカウント作成の流れ
からAWSアカウントを作成するが、先にメールアドレス(Googleアカウント)の作成からしていく。
実際、複数のメールアドレスで複数のAWSアカウントを作成できてしまうので、練習でAWSを使用する場合などを考えて、メールアドレスを複数取得しておけば便利。
画像では、スマホからGoogleアカウント(メールアドレス)を取得しているが好きな方法で取得すれば良いと思う。
ショートメールで送られてくる数字を記入して確認を押す
!

これらを繰り返して複数アドレスを作成しておくと、色々と便利
次にAWSアカウントを作成
最初の画面
作成したメールアドレスと考えたパスワードを記入。AWSアカウントはプロジェクト名など好きなものを記入すると良い。

個人で使用する場合などはパーソナルを選んで住所などの必要情報を記入。
複数回アカウントを作成する場合でも同じ情報を使用してもアカウントは作成できる。

クレジットカード情報を記入。
複数回アカウントを作成する場合でも同じ情報を使用してもアカウントは作成できる。

電話番号を記入して送信を押す。
電話番号に電話が鳴るので電話番号の確認がとれ、ブラウザに表示された4桁の番号を記入欄に記入

ベーシックプランを選択で、1年間無料でAWSのサービスが使用できる

次の画面で完了画面になり、コンソールへログインで登録したメールアドレスとパスワードでログインできる

IAMの設定とアクセスキー、シークレットアクセスキーの取得
AWSアカウントを取得したら速攻でやっておくべき初期設定まとめ
こちらの記事を参考に必須事項のIAMの設定をする

- ルートアカウントのMAFを有効化
- 個々のIAMユーザーの作成
- グループを使用したアクセス許可の割り当て
- IAMパスワードポリシーの適用
わかりやすく言うと
-
AWSアカウントを二段階認証化(通常のAWSアカウントに二段階認証アプリとか使ってサインインできるようにする)
-
IAMユーザ作成(通常のAWSアカウントは使わず、このユーザーでパスワードを設定、二段階認証を設定(MFA)して、そのパスワードや二段階認証でサインインするようにする)
-
IAMグループの作成(グループごとに権限を設定して、そこにユーザーを所属させる)
- AdministratorAccess(AWSアカウントに次ぐ強力な権限)
- AmazonS3FullAccess(S3をフルに使える)
-
IAMパスワードポリシーの適用(パスワードのルールを決める)
まとめると、作成したIAMユーザーと二段階認証でサインイン(ユーザー名、パスワード、二段階認証番号)できるようにして、そのサインインしたユーザーは限定された機能しか使えないグループに所属させることにより、このIAMユーザーでAWSを使用していくように設定する。という言い方で間違いないと思う。
ここのIAMユーザーをグループに所属させた時に、アクセスキーとシークレットアクセスキーを取得しておく
S3のバケットの作成

バケット名に好きな名前をつける、リージョンはTokyo、作成を押すと作成される

Rails側の実装
ImageMagickの導入
$ brew install imagemagick@6
gem編集
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.5'# コメントアウトを外す
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 3.0'
# Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1.7'
# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development
# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
gem 'rack-cors'# コメントアウトを外す
###############################
# ユーザー認証をするために追加したgem
gem 'omniauth'
gem 'devise'
gem 'devise_token_auth'
# 画像をS3に保存をするために追加したgem
# brew install imagemagick@6でimagemagickのバージョン6をインストールしないとrmagickが使えない模様
gem 'carrierwave'# , github: 'carrierwaveuploader/carrierwave'
gem 'fog-aws'
gem 'rmagick'
###############################
gemのインストール
$ bundle install
アップローダーを作成
名前を変えて幾つでもアップローダーを作成できる
$ rails g uploader Image
アップローダーを編集
名前の違うアップローダーを作成した場合は、設定の違うアップの仕方ができる。
store_dirメソッド内で好きなフォルダー名を設定する
class ImageUploader < CarrierWave::Uploader::Base
# Include RMagick or MiniMagick support:
# リサイズしたり画像形式を変更するのに必要
include CarrierWave::RMagick# コメントを外す
# include CarrierWave::MiniMagick
# ストレージの設定
# storage :file# コメント
# storage :fog# コメントを外す
if Rails.env.development?# 開発環境だった場合
storage :fog# 開発が終わったらfileに戻す
elsif Rails.env.test?# テスト環境だった場合
storage :file
else# それ以外の環境だった場合
storage :fog
end
# iPhoneから画像投稿した際に、画像の向きがおかしい場合があるので、
# Rmagickのauto_orientメソッドで向きを正す。
process :fix_rotate
def fix_rotate
manipulate! do |img|
img = img.auto_orient
img = yield(img) if block_given?
img
end
end
# S3のディレクトリ名(フォルダー名)# 格納するディレクトリを指定
def store_dir
# #{}で文字列挿入
# "uploads/#①{model.class.to_s.underscore}/#②{mounted_as}/#③{model.id}"# uploads/①モデル名/②カラム名/③モデルID
"ディレクトリ名/#{model.id}"
# "#{model.id}"# コメント
end
# キャッシュを格納ディレクトリを指定
# def cache_dir
# "cache"
# end
# デフォルト画像は1200x5000に収まるようリサイズ
process resize_to_limit: [1200, 5000]
# サムネイル画像だった場合
# version :thumb do
# process resize_to_fill: [100, 100]
# end
# 許可する画像の拡張子
def extension_whitelist
%w(jpg jpeg gif png)
end
# 保存するファイルの命名規則。上のディレクトリー+このファイル名で保存される
def filename
# 下のトークンを作成するメソッドでファイル名を作るパターンのコード
# "#{secure_token(10)}.#{file.extension}" if original_filename.present?
original_filename if original_filename# 送られて来たファイル名をダイレクトに使う
end
# 一意となるトークンを作成。特に使わないかも
# ファイル名で使うメソッド
# protected
# def secure_token(length=16)
# var = :"@#{mounted_as}_secure_token"
# model.instance_variable_get(var) or model.instance_variable_set(var, SecureRandom.hex(length/2))
# end
end
ModelにUploaderを紐つける
Postのpost_imageカラムにImageUploaderを紐つける
class Api::Post < ApplicationRecord
# どのカラムにどのアップローダーを使うか
# Postのpost_imageカラムに対して、アップローダーのImageUploaderの設定でアップする
mount_uploader :post_image, ImageUploader
# S3の画像を削除
before_destroy :clean_s3
private
# S3の画像を削除
def clean_s3
image.remove! #オリジナルの画像を削除
image.thumb.remove! #thumb画像を削除
rescue Excon::Errors::Error => error
puts "Something gone wrong"
false
end
end
carrierwave.rbの作成
IAMユーザー設定時に取得してきたアクセスキーとシークレットアクセスキーをここで使用
S3で作成したバケット名もここで使用する
CarrierWave.configure do |config|
config.fog_provider = 'fog/aws'
config.fog_credentials = {
#AWSに保存するために必要
# IAMの設定
# アクセスキー
# シークレットアクセスキー
provider: 'AWS',
# アクセスキー
aws_access_key_id: 'アクセスキー',
# シークレットキー
aws_secret_access_key: 'シークレットキー',
# Tokyo
region: 'ap-northeast-1'
# host: 's3.example.com',
# endpoint: 'https://s3.example.com:8080'
}
# 公開・非公開の切り替え# S3のURLに直アクセスしていいか
config.fog_public = true
# キャッシュをS3に保存
# config.cache_storage = :fog
# キャッシュの保存期間(1年)
# config.fog_attributes = { 'Cache-Control' => "max-age=#{365.day.to_i}" }
# キャッシュの保存場所
# config.cache_dir = 'tmp/image-cache'
# S3のURLに有効期限を60秒で設定する
# config.fog_authenticated_url_expiration = 60
# S3バケットを指定
# config.fog_directory = 'BUCKET_NAME(ENV)'
# 環境ごとにS3のバケットを指定
#
# 保存先URL
# asset_host名/fog_directory(バケット)名/uploaderに定義したディレクトリー名/postID/送られてくるファイル名
case Rails.env
when 'production'
config.fog_directory = 'バケット名'# バケット名
config.asset_host = 'https://バケット名.s3-ap-northeast-1.amazonaws.com'
when 'development'
config.fog_directory = 'バケット名'# バケット名
config.asset_host = 'https://バケット名.s3-ap-northeast-1.amazonaws.com'
when 'test'
config.fog_directory = 'バケット名'# バケット名
config.asset_host = 'https://バケット名.s3-ap-northeast-1.amazonaws.com'
end
end
# 日本語ファイル名の設定# 日本語入力を可能にするため。
CarrierWave::SanitizedFile.sanitize_regexp = /[^[:word:]\.\-\+]/
Swift側の実装
ストーリーボードとの接続と、各種変数
画像表示用のUIImageViewと、画像関係の変数を追加
@IBOutlet weak var idTextField: UITextField!
@IBOutlet weak var imageView: UIImageView!
var accesstoken: String!
var client: String!
var uid:String!
var webuser: Webuser!
//画像を保存する際に取得する画像名を入れる変数
fileprivate var postImagerandomString: String!
//画像を保存する際に取得するタイトルを入れる変数
fileprivate var titleNamerandomString: String!
var posts = [Post]()//全てのPostデータを入れる変数
var myposts = [Post]()//自分のPostデータを入れる変数
送信部分
画像送信用のボタンを追加
PostViewController画面から各種データの送信を行う
//画像とpostのパラメーターを保存
@IBAction func postImageSaveButton_Clicked(_ sender: AnyObject) {
//画像名を作る
postImagerandomString = generate(length: 30) as String
//タイトルを作る
titleNamerandomString = generate(length: 30) as String
//画像を用意してね!
let image = UIImage(named: "IMG_0002.JPG")
let data = UIImageJPEGRepresentation(image!, 1.0)
//画像を保存する関数を実行する
self.createImage(postimagedata: data!, fileName: postImagerandomString)
}
画像保存関数
//dataには画像、wihtNameには保存するカラム名、fileNameにはランダムに作った画像名を入れる
func createImage(postimagedata: Data, fileName: String) {
//ログインやサインアップでこれらの情報を取ってこれていた場合は
if self.accesstoken != nil && self.client != nil && self.uid != nil {
let accesstoken: String = self.accesstoken//optionalをStringに変換
let client: String = self.client//optionalをStringに変換
let uid: String = self.uid//optionalをStringに変換
//取り出したトークン情報をヘッダーに格納
var headers: [String : String] = [:]
headers["access-token"] = accesstoken
headers["client"] = client
headers["uid"] = uid
print("accesstoken: \(String(describing: accesstoken))")
print("client: \(String(describing: client))")
print("uid: \(String(describing: uid))")
Alamofire.upload(multipartFormData: { (multipartFormData) in
//画像を送りたい場合はこれを使用*******************************
//複数個指定可
//dataにはData型でUIImageJPEGRepresentationなどでUIImageから変換する
//withNameはサーバーで受け取る際、使用する名前。つまりはRails側のカラム名
//fileNameはファイルの名前。
//mimeTypeはファイルのフォーマット
multipartFormData.append(postimagedata, withName: "post_image", fileName: "\(fileName).jpeg", mimeType: "image/jpeg")
/*
//送りたいテキストがある場合はこれを使用*************************
//テキストをdata型に変換
if let data = "文字列".data(using: String.Encoding.utf8) {
//withNameはサーバーで受け取る際、使用する名前。つまりはRails側のカラム名
multipartFormData.append(data, withName: "name")
}
*/
//ここにRailsのコントローラーアクションへのルートを入れる
}, to: "http://localhost:3000/api/posts", method: .post, headers: headers) { (encodingResult) in
switch encodingResult {
case .success(request: let upload, streamingFromDisk: _, streamFileURL: _):
upload.responseJSON { response in
//debugPrint(response)
print("Request: \(String(describing: response.request))")
print("Response: \(String(describing: response.response))")
print("Error: \(String(describing: response.error))")
let json:JSON = JSON(response.result.value ?? kill)
//保存した画像のpostのIDを取り出す。次に保存(更新)するpostデータのために必要
let postid = json["id"].intValue
print("id: \(String(describing: postid))")
//データを保存
self.createNewPost(id: postid)
}
case .failure(let encodingError):
print("エラーだよ")
print(encodingError)
}
}
}
}
画像を新規で保存した後なので、データの保存は更新で保存する
データの保存後、Postのパラメータセット時に画像保存先のURLをpostImageで保持させるのだが、URL作成にRails側で設定したバケット名やディレクトリー名を使用するので注意
//postデータを保存する関数
func createNewPost(id: Int) {
print("画像は\(postImagerandomString)")
print("タイトルは\(titleNamerandomString)")
let accesstoken: String = self.accesstoken//optionalをStringに変換
let client: String = self.client//optionalをStringに変換
let uid: String = self.uid//optionalをStringに変換
//取り出したトークン情報をヘッダーに格納
var headers: [String : String] = [:]
headers["access-token"] = accesstoken
headers["client"] = client
headers["uid"] = uid
//パラメーターを辞書で入れていく//左辺がカラム名、右辺が値
var params: [String: Any] = [:]
params["web_user_id"] = self.webuser.web_user_id
params["title_name"] = self.postImagerandomString
//画像アップロードでcreateをしたのでデータを作成する場合はcreateしたレコードのIDにアップデートする
//PUT /api/posts/:id/update_post_id(.:format)
Alamofire.request("http://localhost:3000/api/posts/\(id)/update_post_id", method: .put, parameters: params, headers: headers).responseJSON { response in
print("Request: \(String(describing: response.request))")
print("Response: \(String(describing: response.response))")
print("Error: \(String(describing: response.error))")
//型を指定した定数にいれないと比較演算子が使えない
let res: Int = (response.response?.statusCode)!
//アクセスできた場合(更新ができた場合)
if res >= 200 && res < 300 {
print("通信に成功しました")
print("post saved")
print("Request: \(String(describing: response.request))")
print("Response: \(String(describing: response.response))")
print("Error: \(String(describing: response.error))")
print("アップデートなので、レスポンスデータはありません")
let newPost = Post()
//パラメーターセット
newPost.id = id
newPost.web_user_id = self.webuser.web_user_id
newPost.postImage = "https://バケット名.s3-ap-northeast-1.amazonaws.com/fog_directory(バケット)名/uploaderに定義したディレクトリー名/\(id)/\(self.postImagerandomString).jpeg"
newPost.titlename = self.titleNamerandomString
self.myposts.append(newPost)
//アクセスできなかった場合
} else {
print("アクセスできませんでした")
}
}
}
受信部分
画像の取得をパラメーターのpostImageのURLで取得する
index、show、mypostshowボタンで取得したpostの最後のpostのpostImageで画像を取得している
//画像の取得
@IBAction func postImagegetButton_Clicked(_ sender: AnyObject) {
// タイムアウトの設定
let TIME_OUT: TimeInterval = 1 * 300
let post = myposts.last!
let url: String = post.postImage
print("url:\(url)")
// URLRequestの生成
let req = URLRequest(url: NSURL(string: url)! as URL,
cachePolicy: .returnCacheDataElseLoad,
timeoutInterval: TIME_OUT)
let conf = URLSessionConfiguration.default
// URLSessionの生成
let session = URLSession(configuration: conf, delegate: nil, delegateQueue: OperationQueue.main)
session.dataTask(with: req, completionHandler:
{ imageData, resp, err in
// 通信結果
if err == nil {
// Success
// 画像表示
self.imageView.image = UIImage(data: imageData!)!
self.view.addSubview(self.imageView)
}else{
// Error
print("エラーです")
//self.imageView.image = UIImage(named: "代わりの画像")
}
}).resume()
}
AWS解約(おまけ)
最後にAWSの無料利用枠が1年なので、練習としてAWSを利用する場合などは利用料を徴収されては割に合わないので、利用料を徴収されてしまわないように停止しておきたい。