Skip to content

Instantly share code, notes, and snippets.

@rcook
Last active March 30, 2020 05:47
Show Gist options
  • Select an option

  • Save rcook/da0443e277d2c25800930e4599cf030d to your computer and use it in GitHub Desktop.

Select an option

Save rcook/da0443e277d2c25800930e4599cf030d to your computer and use it in GitHub Desktop.
AWS via Haskell Part 6 (EC2)
executable ec2-app
default-language: Haskell2010
hs-source-dirs: ec2
main-is: Main.hs
ghc-options: -threaded -rtsopts -with-rtsopts=-N -W -Wall -fwarn-incomplete-patterns -fwarn-unused-imports
build-depends: amazonka-ec2
, aws-via-haskell
, base >= 4.7 && < 5
, bytestring
, directory
, filepath
, lens
, text
other-modules: EC2Imports
module EC2Imports
( InstanceType(..)
, asgiGroupId
, asgiIPPermissions
, authorizeSecurityGroupIngress
, createSecurityGroup
, csgrsGroupId
, describeImages
, describeInstances
, describeInstanceStatus
, describeKeyPairs
, describeSecurityGroups
, deseImageIds
, desrsImages
, diiInstanceIds
, dirsReservations
, disInstanceIds
, dkprsKeyPairs
, dsgrsSecurityGroups
, dsgsGroupNames
, ec2
, iDescription
, iImageId
, importKeyPair
, insInstanceId
, insPublicDNSName
, instanceRunning
, instanceStatusOK
, ipFromPort
, ipIPRanges
, ipPermission
, ipRange
, ipToPort
, kpiKeyName
, rInstanceType
, rInstances
, rKeyName
, rReservationId
, rSecurityGroupIds
, runInstances
, sgGroupId
) where
import Network.AWS.EC2
The MIT License (MIT)
Copyright (c) 2018 Richard Cook
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
module Main (main) where
import AWSViaHaskell (Endpoint(..), awsConfig, connect, withAWS, wrapAWSService)
import AWSViaHaskell.Prelude
import Control.Exception.Lens (handling)
import Control.Monad (forM_, void)
import Control.Lens ((^.), (&), (.~), Getting)
import Data.ByteString (ByteString)
import qualified Data.ByteString as ByteString (readFile)
import Data.Maybe (catMaybes, fromMaybe)
import Data.Monoid ((<>), First)
import Data.Text (Text)
import qualified Data.Text as Text (pack)
import qualified Data.Text.IO as Text (putStrLn)
import EC2Imports
import System.Directory (getHomeDirectory)
import System.FilePath ((</>))
wrapAWSService 'ec2 "EC2Service" "EC2Session"
newtype DNSName = DNSName Text
newtype GroupId = GroupId Text
newtype GroupName = GroupName Text
newtype ImageDescription = ImageDescription (Maybe Text)
newtype ImageId = ImageId Text
newtype InstanceId = InstanceId Text
newtype KeyMaterial = KeyMaterial ByteString
newtype KeyName = KeyName Text
newtype ReservationId = ReservationId Text
-- Amazon Linux AMI 2017.09.1 (HVM), SSD Volume Type
freeTierImageId :: ImageId
freeTierImageId = ImageId "ami-caaf84af"
mySecurityGroupName :: GroupName
mySecurityGroupName = GroupName "my-security-group"
keyName :: KeyName
keyName = KeyName "My key"
doImportKeyPairIfNotExists :: KeyName -> KeyMaterial -> EC2Session -> IO ()
doImportKeyPairIfNotExists (KeyName kn) (KeyMaterial km) = withAWS $
handling _DuplicateKeyPair (const $ pure ()) $
void $ send $ importKeyPair kn km
where
_DuplicateKeyPair :: AsError a => Getting (First ServiceError) a ServiceError
_DuplicateKeyPair = _ServiceError . hasStatus 400 . hasCode "InvalidKeyPair.Duplicate"
doDescribeKeyPairs :: EC2Session -> IO [KeyName]
doDescribeKeyPairs = withAWS $ do
result <- send describeKeyPairs
return $ catMaybes [ KeyName <$> (keyPair ^. kpiKeyName) | keyPair <- result ^. dkprsKeyPairs ]
doCreateSecurityGroupIfNotExists :: GroupName -> EC2Session -> IO GroupId
doCreateSecurityGroupIfNotExists (GroupName gn) = withAWS $ do
handling _DuplicateGroup (const getExisting) createNew
where
_DuplicateGroup :: AsError a => Getting (First ServiceError) a ServiceError
_DuplicateGroup = _ServiceError . hasStatus 400 . hasCode "InvalidGroup.Duplicate"
getExisting = do
result <- send $ describeSecurityGroups
& dsgsGroupNames .~ [ gn ]
return $ GroupId ((head $ result ^. dsgrsSecurityGroups) ^. sgGroupId)
createNew = do
csgResult <- send $ createSecurityGroup gn "my-security-group-description"
let gid = csgResult ^. csgrsGroupId
inboundSSHPermission = ipPermission "tcp"
& ipFromPort .~ Just 22
& ipToPort .~ Just 22
& ipIPRanges .~ [ ipRange "0.0.0.0/0" ]
void $ send $ authorizeSecurityGroupIngress
& asgiGroupId .~ Just gid
& asgiIPPermissions .~ [ inboundSSHPermission ]
return $ GroupId gid
doDescribeImages :: [ImageId] -> EC2Session -> IO [(ImageId, ImageDescription)]
doDescribeImages imageIds = withAWS $ do
result <- send $ describeImages
& deseImageIds .~ map (\(ImageId iid) -> iid) imageIds
return $ [ (ImageId (i ^. iImageId), ImageDescription (i ^. iDescription)) | i <- result ^. desrsImages ]
doRunInstance :: ImageId -> KeyName -> GroupId -> EC2Session -> IO (ReservationId, InstanceId)
doRunInstance (ImageId iid) (KeyName kn) (GroupId gid) = withAWS $ do
result <- send $ runInstances iid 1 1
& rKeyName .~ Just kn
& rInstanceType .~ Just T2_Micro
& rSecurityGroupIds .~ [ gid ]
let reservationId = ReservationId (result ^. rReservationId)
inst = head $ result ^. rInstances
instId = InstanceId (inst ^. insInstanceId)
return (reservationId, instId)
doWaitUntilInstanceRunning :: InstanceId -> EC2Session -> IO ()
doWaitUntilInstanceRunning (InstanceId iid) = withAWS $
void $ await instanceRunning (describeInstances & diiInstanceIds .~ [ iid ])
doWaitUntilInstanceStatusOK :: InstanceId -> EC2Session -> IO ()
doWaitUntilInstanceStatusOK (InstanceId iid) = withAWS $
void $ await instanceStatusOK (describeInstanceStatus & disInstanceIds .~ [ iid ])
doGetPublicDNSName :: InstanceId -> EC2Session -> IO (Maybe DNSName)
doGetPublicDNSName (InstanceId iid) = withAWS $ do
result <- send $ describeInstances
& diiInstanceIds .~ [ iid ]
let res = head $ result ^. dirsReservations
inst = head $ res ^. rInstances
mbDNSName = inst ^. insPublicDNSName
return $ DNSName <$> mbDNSName
main :: IO ()
main = do
homeDir <- getHomeDirectory
let publicKeyPath = homeDir </> ".ssh" </> "id_rsa.pub"
privateKeyPath = homeDir </> ".ssh" </> "id_rsa"
ec2Session <- connect
(awsConfig (AWSRegion Ohio))
ec2Service
putStrLn "ImportKeyPair"
publicKeyMaterial <- KeyMaterial <$> ByteString.readFile publicKeyPath
doImportKeyPairIfNotExists keyName publicKeyMaterial ec2Session
putStrLn "DescribeKeyPairs"
keyNames <- doDescribeKeyPairs ec2Session
forM_ keyNames $ \(KeyName kn) -> Text.putStrLn $ " " <> kn
putStrLn "CreateSecurityGroup"
groupId@(GroupId gid) <- doCreateSecurityGroupIfNotExists mySecurityGroupName ec2Session
Text.putStrLn $ " " <> gid
putStrLn "DescribeImages"
infos <- doDescribeImages [freeTierImageId] ec2Session
forM_ infos $ \(ImageId imageId, ImageDescription description) ->
Text.putStrLn $ " " <> imageId <> ": " <> fromMaybe "(no description)" description
putStrLn "RunInstances"
(ReservationId rid, instId@(InstanceId iid)) <- doRunInstance freeTierImageId keyName groupId ec2Session
Text.putStrLn $ " Reservation ID: " <> rid
Text.putStrLn $ " Instance ID: " <> iid
putStrLn "doWaitUntilInstanceRunning (please be patient!)"
doWaitUntilInstanceRunning instId ec2Session
putStrLn "doWaitUntilInstanceStatusOK (please be patient!)"
doWaitUntilInstanceStatusOK instId ec2Session
putStrLn "doGetPublicDNSName"
mbDNSName <- doGetPublicDNSName instId ec2Session
case mbDNSName of
Nothing -> putStrLn "(Instance has not public DNS name)"
Just (DNSName dn) -> do
putStrLn "Command line to connect to instance:"
let commandLine = "ssh -i " <> Text.pack privateKeyPath <> " ec2-user@" <> dn
Text.putStrLn commandLine
putStrLn "Done"
newtype DNSName = DNSName Text
newtype GroupId = GroupId Text
newtype GroupName = GroupName Text
newtype ImageDescription = ImageDescription (Maybe Text)
newtype ImageId = ImageId Text
newtype InstanceId = InstanceId Text
newtype KeyMaterial = KeyMaterial ByteString
newtype KeyName = KeyName Text
newtype ReservationId = ReservationId Text
module AWSViaHaskell.Prelude
( _ServiceError
, AsError
, Credentials(..)
, Region(..)
, ServiceError
, await
, hasCode
, hasStatus
, send
, sinkBody
, toText
) where
import Network.AWS
( _ServiceError
, AsError
, Credentials(..)
, Region(..)
, ServiceError
, await
, send
, sinkBody
)
import Network.AWS.Data (toText)
import Network.AWS.Error
( hasCode
, hasStatus
)
wrapAWSService 'ec2 "EC2Service" "EC2Session"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment